From 6656528d782d06a5714094a2cb255620e7de7769 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 27 Nov 2024 19:43:52 -0500 Subject: [PATCH 001/112] TUNIC: Fix missing ladder rule for library fuse #4271 --- worlds/tunic/er_rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 3b111ad83488..d5d6f16c57ec 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1446,7 +1446,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("West Garden Fuse"), lambda state: has_ability(prayer, state, world)) set_rule(world.get_location("Library Fuse"), - lambda state: has_ability(prayer, state, world)) + lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world)) # Bombable Walls for location_name in bomb_walls: From a3711eb463c64dcd0b6070bb7d0c7ca796250f51 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 28 Nov 2024 01:46:06 +0100 Subject: [PATCH 002/112] Launcher: fix detection of valid .apworld (#4272) --- worlds/LauncherComponents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 3c4c4477ef09..67806a7394c7 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -103,7 +103,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path try: import zipfile zip = zipfile.ZipFile(apworld_path) - directories = [f.filename.strip('/') for f in zip.filelist if f.CRC == 0 and f.file_size == 0 and f.filename.count('/') == 1] + directories = [f.name for f in zipfile.Path(zip).iterdir() if f.is_dir()] if len(directories) == 1 and directories[0] in apworld_path.stem: module_name = directories[0] apworld_name = module_name + ".apworld" From f4322242a1959f707cd6db088ae12f19d49ac214 Mon Sep 17 00:00:00 2001 From: nmorale5 <76963132+nmorale5@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:43:37 -0500 Subject: [PATCH 003/112] Pokemon RB - Fix Incorrect Item Location in Victory Road 2F (#4260) --- worlds/pokemon_rb/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 5885183baa9c..943204ceaf75 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -223,7 +223,7 @@ def __init__(self, flag): Missable(92)), LocationData("Victory Road 2F-C", "East Item", "Full Heal", rom_addresses["Missable_Victory_Road_2F_Item_2"], Missable(93)), - LocationData("Victory Road 2F-W", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"], + LocationData("Victory Road 2F-C", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"], Missable(94)), LocationData("Victory Road 2F-NW", "North Item Near Moltres", "Guard Spec", rom_addresses["Missable_Victory_Road_2F_Item_4"], Missable(95)), From 4395c608e80c6bdd2d219bdf32b5dc807c8a3779 Mon Sep 17 00:00:00 2001 From: Tim Mahan <60069210+Bicoloursnake@users.noreply.github.com> Date: Thu, 28 Nov 2024 02:41:13 -0500 Subject: [PATCH 004/112] [Docs] Update the macOS guide to match changes in core (#4265) * Update mac_en.md Updated the minimum version recommended to a version actually supported by AP. * 3.13 is not in fact, supported. Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/generic/docs/mac_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md index 2904781862da..76b1ee4a3827 100644 --- a/worlds/generic/docs/mac_en.md +++ b/worlds/generic/docs/mac_en.md @@ -2,8 +2,8 @@ Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal. ## Prerequisite Software Here is a list of software to install and source code to download. -1. Python 3.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). - **Python 3.11 is not supported yet.** +1. Python 3.10 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). + **Python 3.13 is not supported yet.** 2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835). 3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases). 4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases). From a07ddb4371334619e3ea909b6f50cb2dfbd351db Mon Sep 17 00:00:00 2001 From: Lolo Date: Thu, 28 Nov 2024 17:13:14 +0100 Subject: [PATCH 005/112] Docs: (Re)write french alttp setup guide and game page (#2296) --- worlds/alttp/docs/fr_A Link to the Past.md | 32 +++ worlds/alttp/docs/multiworld_fr.md | 204 +++++------------- .../docs/retroarch-network-commands-fr.png | Bin 0 -> 20461 bytes 3 files changed, 91 insertions(+), 145 deletions(-) create mode 100644 worlds/alttp/docs/fr_A Link to the Past.md create mode 100644 worlds/alttp/docs/retroarch-network-commands-fr.png diff --git a/worlds/alttp/docs/fr_A Link to the Past.md b/worlds/alttp/docs/fr_A Link to the Past.md new file mode 100644 index 000000000000..a9ff8646b3f2 --- /dev/null +++ b/worlds/alttp/docs/fr_A Link to the Past.md @@ -0,0 +1,32 @@ +# A Link to the Past + +## Où se trouve la page des paramètres ? + +La [page des paramètres du joueur pour ce jeu](../player-options) contient tous les paramètres dont vous avez besoin +pour configurer et exporter le fichier. + +## Quel est l'effet de la randomisation sur ce jeu ? + +Les objets que le joueur devrait normalement obtenir au cours du jeu ont été déplacés. Il y a tout de même une logique +pour que le jeu puisse être terminé, mais dû au mélange des objets, le joueur peut avoir besoin d'accéder à certaines +zones plus tôt que dans le jeu original. + +## Quels sont les objets et endroits mélangés ? + +Tous les objets principaux, les collectibles et munitions peuvent être mélangés, et tous les endroits qui +pourraient contenir un de ces objets peuvent avoir leur contenu modifié. + +## Quels objets peuvent être dans le monde d'un autre joueur ? + +Un objet pouvant être mélangé peut être aussi placé dans le monde d'un autre joueur. Il est possible de limiter certains +objets à votre propre monde. + +## À quoi ressemble un objet d'un autre monde dans LttP ? + +Les objets appartenant à d'autres mondes sont représentés par une Étoile de Super Mario World. + +## Quand le joueur reçoit un objet, que ce passe-t-il ? + +Quand le joueur reçoit un objet, Link montrera l'objet au monde en le mettant au-dessus de sa tête. C'est bon pour +les affaires ! + diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md index 310f3a4f96c4..0638d843e810 100644 --- a/worlds/alttp/docs/multiworld_fr.md +++ b/worlds/alttp/docs/multiworld_fr.md @@ -1,41 +1,28 @@ # Guide d'installation du MultiWorld de A Link to the Past Randomizer -
- -
- ## Logiciels requis -- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) -- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents) +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). +- [SNI](https://github.com/alttpo/sni/releases). Inclus avec l'installation d'Archipelago ci-dessus. + - SNI n'est pas compatible avec (Q)Usb2Snes. - Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES - - Un émulateur capable d'éxécuter des scripts Lua - ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), - [BizHawk](https://tasvideos.org/BizHawk)) - - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle - compatible -- Le fichier ROM de la v1.0 japonaise, sûrement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc` + - Un émulateur capable de se connecter à SNI + [snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), + [BSNES-plus](https://github.com/black-sliver/bsnes-plus), + [BizHawk](https://tasvideos.org/BizHawk), ou + [RetroArch](https://retroarch.com?page=platforms) 1.10.1 ou plus récent). Ou, + - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle compatible. **À noter: + les SNES minis ne sont pas encore supportés par SNI. Certains utilisateurs rapportent avoir du succès avec QUsb2Snes pour ce système, + mais ce n'est pas supporté.** +- Le fichier ROM de la v1.0 japonaise, habituellement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc` ## Procédure d'installation -### Installation sur Windows - -1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer - la version la plus récente. - **Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties - classiques de multiworld, téléchargez `Setup.BerserkerMultiWorld.exe` - - Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le - fichier - `Setup.BerserkerMultiWorld.Doors.exe`. - - Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà - installé le logiciel auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale - ne sera pas requise. - - Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement - parce qu'un jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer. - -2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme +1. Téléchargez et installez [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). **L'installateur se situe dans la section "assets" en bas des informations de version**. + +2. Si c'est la première fois que vous faites une génération locale ou un patch, il vous sera demandé votre fichier ROM de base. Il s'agit de votre fichier ROM Link to the Past japonais. Cet étape n'a besoin d'être faite qu'une seule fois. + +3. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme programme par défaut pour ouvrir vos ROMs. 1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez. 2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...** @@ -44,58 +31,6 @@ 5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier devrait se trouver dans le dossier que vous avez extrait à la première étape. -### Installation sur Mac - -- Des volontaires sont recherchés pour remplir cette section ! Contactez **Farrak Kilhn** sur Discord si vous voulez - aider. - -## Configurer son fichier YAML - -### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? - -Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur -comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra fournir son propre fichier YAML. Cela permet -à chaque joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld -peuvent avoir différentes options. - -### Où est-ce que j'obtiens un fichier YAML ? - -La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos -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](/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. - -Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "On" et quarante pour "Off". Quand le -générateur décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un -papier dans le seau. Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé. - -S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour -chaque paramètre il faut au moins une option qui soit paramétrée sur un nombre strictement positif. - -### Vérifier son fichier YAML - -Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du -[Validateur de YAML](/check). - -## Générer une partie pour un joueur - -1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options, - et cliquez sur le bouton "Generate Game". -2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch. -3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client - n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI). - -## Rejoindre un MultiWorld - ### Obtenir son patch et créer sa ROM Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier YAML à celui qui héberge la partie ou @@ -109,35 +44,58 @@ automatiquement le client, et devrait créer la ROM dans le même dossier que vo #### Avec un émulateur -Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiquement également en arrière-plan. Si +Quand le client se lance automatiquement, SNI devrait se lancer automatiquement également en arrière-plan. Si c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu Windows. +#### snes9x-nwa + +1. Cliquez sur 'Network Menu' et cochez **Enable Emu Network Control** +2. Chargez votre ROM si ce n'est pas déjà fait. + ##### snes9x-rr 1. Chargez votre ROM si ce n'est pas déjà fait. 2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting** 3. Cliquez alors sur **New Lua Script Window...** 4. Dans la nouvelle fenêtre, sélectionnez **Browse...** -5. Dirigez vous vers le dossier où vous avez extrait snes9x-rr, allez dans le dossier `lua`, puis - choisissez `multibridge.lua` -6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom - dans le coin en haut à gauche. +5. Sélectionnez le fichier lua connecteur inclus avec votre client + - Recherchez `/SNI/lua/` dans votre fichier Archipelago. +6. Si vous avez une erreur en chargeant le script indiquant `socket.dll missing` ou similaire, naviguez vers le fichier du +lua que vous utilisez dans votre explorateur de fichiers et copiez le `socket.dll` à la base de votre installation snes9x. + +#### BSNES-Plus + +1. Chargez votre ROM si ce n'est pas déjà fait. +2. L'émulateur devrait automatiquement se connecter lorsque SNI se lancera. ##### BizHawk -1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant +1. Assurez vous d'avoir le cœur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant ces options de menu : - `Config --> Cores --> SNES --> BSNES` - Une fois le coeur changé, vous devez redémarrer BizHawk. + - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES` + - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+` + Une fois le cœur changé, rechargez le avec Ctrl+R (par défaut). 2. Chargez votre ROM si ce n'est pas déjà fait. -3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console** -4. Cliquez sur le bouton pour ouvrir un nouveau script Lua. -5. Dirigez vous vers le dossier d'installation des utilitaires du MultiWorld, puis dans les dossiers suivants : - `QUsb2Snes/Qusb2Snes/LuaBridge` -6. Sélectionnez `luabridge.lua` et cliquez sur "Open". -7. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom - dans le coin en haut à gauche. +3. Glissez et déposez le fichier `Connector.lua` que vous avez téléchargé ci-dessus sur la fenêtre principale EmuHawk. + - Recherchez `/SNI/lua/` dans votre fichier Archipelago. + - Vous pouvez aussi ouvrir la console Lua manuellement, cliquez sur `Script` 〉 `Open Script`, et naviguez sur `Connecteur.lua` + avec le sélecteur de fichiers. + +##### RetroArch 1.10.1 ou plus récent + +Vous n'avez qu'à faire ces étapes qu'une fois. + +1. Entrez dans le menu principal RetroArch +2. Allez dans Réglages --> Interface utilisateur. Mettez "Afficher les réglages avancés" sur ON. +3. Allez dans Réglages --> Réseau. Mettez "Commandes Réseau" sur ON. (trouvé sous Request Device 16.) Laissez le +Port des commandes réseau à 555355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-fr.png) +4. Allez dans Menu Principal --> Mise à jour en ligne --> Téléchargement de cœurs. Descendez jusqu'a"Nintendo - SNES / SFC (bsnes-mercury Performance)" et + sélectionnez le. + +Quand vous chargez une ROM, veillez a sélectionner un cœur **bsnes-mercury**. Ce sont les seuls cœurs qui autorisent les outils externs à lire les données d'une ROM. #### Avec une solution matérielle @@ -147,10 +105,7 @@ le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger [sur cette page](http://usb2snes.com/#supported-platforms). 1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement. -2. Fermez QUsb2Snes, qui s'est lancé automatiquement avec le client. -3. Lancez la version appropriée de QUsb2Snes (v0.7.16). -4. Lancer votre console et chargez la ROM. -5. Remarquez que l'interface Web affiche "SNES Device: Connected", avec le nom de votre appareil. +2. Lancez votre console et chargez la ROM. ### Se connecter au MultiServer @@ -165,47 +120,6 @@ l'interface Web. ### Jouer au jeu -Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations -pour avoir rejoint un multiworld ! - -## Héberger un MultiWorld - -La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par -[le site](https://berserkermulti.world/generate). Le processus est relativement simple : - -1. Récupérez les fichiers YAML des joueurs. -2. Créez une archive zip contenant ces fichiers YAML. -3. Téléversez l'archive zip sur le lien ci-dessus. -4. Attendez un moment que les seed soient générées. -5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations "Seed Info". -6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres - joueurs afin qu'ils puissent récupérer leurs patchs. - **Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur, - tandis que ceux de la page "Seed Info" non. -7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également - fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant - observer devrait avoir accès à ce lien. -8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer. - -## Auto-tracking - -Si vous voulez utiliser l'auto-tracking, plusieurs logiciels offrent cette possibilité. -Le logiciel recommandé pour l'auto-tracking actuellement est -[OpenTracker](https://github.com/trippsc2/OpenTracker/releases). - -### Installation - -1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le - fichier `.msi`). -2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio - Build Tools". Un lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement. - -### Activer l'auto-tracking - -1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez ** - AutoTracker...** -2. Appuyez sur le bouton **Get Devices** -3. Sélectionnez votre appareil SNES dans la liste déroulante. -4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking** -5. Cliquez sur le bouton **Start Autotracking** -6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire +Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations, +vous venez de rejoindre un multiworld ! Vous pouvez exécuter différentes commandes dans votre client. Pour plus d'informations +sur ces commandes, vous pouvez utiliser `/help` pour les commandes locales et `!help` pour les commandes serveur. diff --git a/worlds/alttp/docs/retroarch-network-commands-fr.png b/worlds/alttp/docs/retroarch-network-commands-fr.png new file mode 100644 index 0000000000000000000000000000000000000000..60eba5b1b0fb5c45f51b6c8728617bd0a9043775 GIT binary patch literal 20461 zcmce;1z44Bw=Rl-3J53yN{N)TC?O5f-3&@nTr#<0#W~!!!N31H>~MF|c!SfpyE}%rvu>A* zg`3{i*0vvck|;8nKQD=pU$EknlfcWnR-yE3@Y?J2*&g|0Lb;_83e7T;Rw?9nTNN!W zQ?pdGZgY6>-HW-|CVF`NfBg-12-e?!ChNZfuO>XgT=0St=p%j;UXr)(AnynY>%;%? z8~#uJm>N#`w@7)l^X_9N3B3FFv$C>gs_dv47}lD7@73<>N;n%av#@MV*Ad;jClnTz zl+>f9{E5#zm9zyIwiDhQf4BvSH)G zgq`MCHgl(7&pYU5_n*A{6)_T$6r)vXg@=ddurWC{GQv(yo@zRfS*yhMfXysCB&2U^ z_PcYP=+~N>n>TLwwdm^4E77sS}3Mu(nbaZsAtgIeBeE1`n zv{1L<;^O?2goMPI-{EwlX7l1~x5sj#V%vp{h}UH=M=5K#P?rhE*GZ|x-ofGg>*h1C|@Q=ipJh*G+gyk=m`XBS)IbM7XZK@iqOZNM$GAI0#x%gGxD_=lceP3@BDPLw`VO>W1*+d{qW%fg;-Dk33nvBibY2_ zZMfm(!u^FSD9IJY7Z=oZKYHw3D%zKaBH1MjvDQOJhWj2}gc#ajlBnqB5^iSD9|qB& z)2kJ{OrfZQyJg+qUs7J4z~gKS4}5)Xt)!&H<9vUFgxhhl-aW4HiIC430gF-X;k0XH zWF##uEj;s+?H{BR6ci-H#F`Zr?c6Fn_EqbG-kIwIU-twF50?kBvgDGmZru`>k}@#I z+Xyt6KNJ%XsS&fcw_hSDH|hJZgSn1o?LH+N8xxbj>l%p;H?Xa{+bA%gv~+j6&XtzQ z#mPy2zsU;&A0Hq6RP>ww^JzGu%l=B8)AsNF3@Prn8?_$iT)Nt#zl@bbB&XSmD=V|+ z)zsBFOnQ^;7Q0pY@4?>iHpE_v-dPtrk&q*v(S1V4kc#h{BG9xwBvOHoNqzBSDfM^C zm+^teHH&*zY9_`xVf#%>BOJLT)XkM)$7VTKZ^o4oJY=daXDDGEa*x%sJ@IyEygOCK zP+u)+c0yE$S9O>%Tdgnv@M=_o~v zH^2GCAXiyLYqZoD8*g%IN&~}rT~@J?%kIzZ^261U;PN(k!Bj(G;p@3{=EHd!>go@u z#K)c7+<3g`#@FiIPit#yov>-&Cn-c6pPckJdmK)^U0z16J5b)i1@{wIPOUd?MS|2&yeye0ev&yM5{=##Z$6+jcZ&x4>dKNEeU5Z zbVHt%)HKV_{F0UQRlhv`$|vD^A5SA@`=N^BKuCd*57vzvHx4&vj8*8YM~mKnxXM^# zzbx3EV?I(~|1N8OadXq!z~FmzOk7-{c1@(<(1@<*RH?C?oLrAior^;H?)G+ZDg9!Q z2-$V4I+H$f3W_w?X;szLC8yJ{^a)u_o|+Fc48Nbk5ko!Zo*zZfy`o+mue9z#Tz$Kf zmE@7S(D?qEM#NoTVcaSjUQ%9{aP~Ss#lhAo|nLmm*QTXIhiI(Qfq zs>n>?tKSg8V=Z!jz8Y9>DxVfoVAvU%t5%pPC@y}>JjLxr-z=Ow)I3HyI^GzGrtjac zUA;Z3*6(Sl_65`|0F)(OPdP5Pc zC)EzCG#y0)re=preH4TrcJAe(Pqr&%$tonLryCj_yls5z=$QG{aermV2b**!Q!S8? z&F}d`0s;wvs^sK%Y>F2cw{LfKcBZAbXkOnC8tTQTAyL#DLa3~TLCejy+zU%oKerLo z*FPFL>yzW#PhT=B-7eZ-X(`+@3|JFr&+C4#G^`cu=XWIVljp2frcn;3_`6(IG$a<@dBY|43x_-@*yn@SbT&^CG1%;_*0rr`hnc=Pv{B(Ey_?1?{VnN*%FE8&TIxWLxz@nNo}`-UYDJY_vN-5^P#Jk#57q(*Sh5lM`7#9K z{r#a8Lf?z|&~<~8yOk~B@)xpxeA*PE>Tn;CiDgaU_l{!H#d^qd1SgfJa3l>tW-vW| zdOei=l0QE*RFw3DNQZH;t9xo-26rr)NXK{4fgnURaks{Wwr858=(8D=1-_U3VfyjM zvlyAwg#_C5xd{nBxaxh)c%;N`8cAX^Gu#`@*NVzFq*3fG`H0hjK4@iO!LY7!k^vX| z(u+rIXLmOzK~0&`W+^*6JDBfe>0o^#F)?w*^O83swXyL3SgCOy zoegz+i~s#39_RjYbG43oW7rNcnXb4u!62XlURLayhQG+0EouzukPYD%$tH1S_}WfyB0- z-*qgR?s(3*g@uKUjg7^{`y?dmzkipwpE@2(#hVGo1VoM$Xd9@^a!X7Fk#Ik*Iq0Zo z(P~#w3yq19D9z%@MgO$Ee zU~h^oZ*^@3)~T|+(S6xt9c8+=@G=S?yP~IaL(8Qgy~FHDe*%=V?gVa!mBA3tZW~3# z(25GjT-AK1x~q9wtHb%ub<(?kemYCYdU)_8wcq0?lLmZfVzN~0yh{_I zczb92`1GT~{qelYN_IRb#v1;T-0crlnQeZ(aA|*c<(jRXoq>VD7{Zo{lCt-+<#^fb zK6*Bn!c|7~qIZ!&K|$=!kko|&nbZr-1cRw&h8-#|<8+5|lWnvKd=HHIfo3W|z)r>7G~G9)D>5oc3c z1d^Q4dFyN!I$#xP_%|NwK~*3jC7prQPd7z z-Z4ChR#(Yzzw?BJWmm`9MCHw!;=J3?OlhUlq&A;P#hrApw1_9#h5hK{?^>5XVc9s& zC*sxQmbrN5AWR&^K*vB=wisNSA4?c$Pc0iSE+J7|S{ioVYHngeLsbNg?`I6lkH*G# zY|pKyCue3nyP}_AVPWOLO{L`Jo%;UW;7?Z!1D2V&xl-clR88eh{woCqg)+bs8EXoU z3JMDR@gt_c(8#s*wzjsiE-fZ`J~?fr-+%elhAe}9tX#qSxkv)HquatgobL8^z|b<` z1tq1WQ4AX7v^D1Pzs}kK+Y5_{8MpZ1<#iF#cO1Z17o6^SHaa4&;|&^-G^?E)x*XD{i%EkG+p$dn=E zfjh0?*PEP&>Q1Z{8Ke*(F>l3gBIlQ!Sn9G8Ld-j$Ga$e}M($o9n1mxt<8e&;n zZyp^rH1rFq=<4crL=B2BD;>v2-u`*pA?*BjWJrhtaGuq{93jJTAB|F@_Qda9x8g~? zGpQaw7Nx|=$jGRws-m6OULd`mCddEu=@+BW>AUVUu#U=DsHmtqqnNTNe{kn~#a=)x zZ3@I}CBhwJTO0nl&PBlavs%SzS4C%4lvc~=zDBI+V*Q|;bVT$6d{(-1{p^;M-Tku< zWt15WiThDKZDq?JbCRQWq^Va$-ha$*9+ao%u1u+H5O5nxG62}s4Qq$_rs3z~-9=J< zk9ao4fas@sS4HkfQsNTQ;H}Rz@YB&rUcCL@RKudJu9g*5D3@5U~5J=}nb*RPdX+Zopp&aDO| zJM6KHKL4~NQ3E_i_&1YwwI~Hs(r_fxZRBq>ry!w4Xi>WhHds_TitIC#+wJbpDXp&Y zo8miYNNs$MnJWBlx5;Z)};w33#-EraXwU~^KA~R!|M~3!R$kypIeTvjhFjRYgSrKIfV@ZQ3ZI> zPG3j}EcquI@?r)hE3K^V*D=@=mn-sbKeKkhbLeevGadfvGm@*smY9)NWSh}N>UpLh zFW+0|`nKa9kd;~93jvyJ?XNcP)pW!>jI5@rRg{$ntL%(*b!R?*{tS3*M|SAdV77v& zsOWbe?8SY;*^P~d4<69q`#Wm~)z$GbnywtZY-{iAG;oJgFe}BmR_AjuAZb5+Vb}w} zBPt{$#YWY;_2gi}8dxemlg`=9C}q*92@w-$NrESAn3us4_7Eve^K7-khI|E;O%Lj;;u1|pD%y|tyK z+!%I!>M0IOvztZI3Z*1FC!OLYwDMZZRi4b%#Cc;9YJR?c`# z6linT%LX;}h)=^pxq16RkEY(?V@@C%X$$ZuH9+ioanE#dUSpj@CpB%i&C|?R zJ-AX8(L24e&AfR;bNMy*N?AH{`IduqawbKBFgb0=ZwE$3$s0fiVBwZkRLDw84-O3M zZf)H{LrY-=l8s!gpdk@+*=MUW)Vm%&S%{9HSCfrp^(`#GArq=J9eB*c<2K(G;_dAX zCz{gtBao=_?WT?f!r;(DLEr+5vqDm(%!}9cK>pj*R6_Zek&V}?s`!QsW}730q~;+6 ztR{CDDaboG@w((IurM&CRa8KTYT9P*EbV4G;22>h(63l#0J@uaM?eF z=8;g5vg3_OL_^bCA{F%{Jigj-ePwl3s=a4sW~Q~(+ku6hUAM-O(F;-7a8YgC12{Rc z&SgJUd~t5hz}Wc5Nw(y#UVnT>W@hH`3d^|MTzXUzcJ|8TsZhrb5 zn}2Vq@1s%q+s)}Dj}uy|&J3xje%IGi*Ec>QMsG%ig$YrJiil`8ck|TXq zjU{hmIyKPc^ofs@7ys*)U2L&Y-etr80y4}dYJ^N6lnEwzvHfq)UTw2P~QVzUa)&7kg)SogV^Cf{|V5~Fz47xC?+t1Ec7i26egA#)8oYOl^XP}Hd zoNtDB)*llatGJ;@c+gzpv(Y5OX^QL~2*t>#4A_vupN$ zOH?uZPg3F6?ESO`L|Nfo25}% zNXE1x+0K#aYD*r(Q3^<-blI722UKNjY+RbSLr18rvA}D(N5pQ$<9QLkgQI9#w4aOj##Tirc(UZH8(vi7& zWXdIN%*_d1-cYZyrLqPdoE?FisGw+K9pU8U)!bZEFZa%L&~O3kZBGF`!eBgg>b9ew zY^Jap^~N~<)Lvd@!LD=I3Sygqgrtdpc6}yhF-n~>T#Lcauk=H zogLl|aeGfsmZOeBNMlM`YASHvw24Yqzr}4e)3dS$LKno5 z%~Sub$29AK<{Of*L8iw$k2$$Cm?&HI7m{L=@qI(osdrJ4=OX+7_kOmY{1fI!RUCrqvrr5JSUs!ybZtSlBLW&-H48H#=ae0(dO zIINb#6E%BR-Cn<@G*>XC9y`u|f9++(3eyj@68(LD!HY=(!13>nv=ZfD9SbRduo6jttL@B0~QrA~Q+7zk-j@Hv7J9Sr#2@MUU^E^~F`t||!KfqtJ zk9s}>YS*oIi{X2$s$xp|DUF5`s%2jq9_j3Nz6Izq#dL68Mtw3p6}uRjO<_H)Mfd~* z(Ya-A8~M=^{dH1>MKPlE(-j4w|2=ah0sv@^hKZSReHh9lr`?|i54L_3v;JqfE2F+4 z0htgyiGk!K&x@)xqQXJD*(R^>>DMEo1mn048>7V(g}DG}|2I(AQBDy;sRIt)@gqPD zJirVkQ#)p?Za}fd%`6^az)FaEK+9*`{rd-tOK_5L{S2Gs_#?*Ug%KQ*bz>Iq*O-od zQZkjdxI3#=WCAZN_zAcj^OBNu6&3rxkO`^Vm`+viM>1+LFfzhUzjyDRjI=Z%8{K(C z8OY)aavEP#WE7U}`nSZy#NZ0A4CNXd8kQUVBt*Z0Z#(mw5;q^TxTU2fb$NN4&1qgZ z0(uD8ZE1PQMN)#^2iw~*eC}L$Gyz72h6*2^v9V?RT3BBP?LxO2j}~!qa-vwDE-IPS zu~_!n#3*|L3Z}+TTU$2ZYtVpFnAg7poW=Fvllpb`l$8-=4)PEhM#i5%f69<;(z(q= z^k$Jhcp%OqvH6(~7aqkpa>*@pMk$5PB`8n&*hZWlzLiyOas^&;jYbMx8pZZIi-HzBR4boA5rY&Kor*M2O zu2@IKgEn2~tBXEkz*#_aUS?F9UXU^DNTAo!(&|vDb3M%bCG~HatIFQtuZ;~^+9XE_ zUEL(bDW--q!BbuLo->dj<?3BVFaKLdCShzqIv*!;61yC;tl%G(8)kfZs$xG5*h(!T*NU zCidzEoAjj;Fd?~f;{^Www?0wnTOVJ+cR*Y3Bc0X%sNMW;-r?WrRfmLy-D5%w0jF`6 zKz#km!OMFNTBe?!9*AeZ@-HPjSi)jroDVmqqN5+g#-E+J)u$3NXq3E$x(&D!+9|0u z;x4F>W98=e>&jsZ0D%E2!(!aM2LA}48FWi>UvgI1w8y8}sg#`rJKST?W2+R;+bPs=z8J`Lnkr=W{05_M)HSVn9& znL9cj_!#mX{rU*jbTCvIPEO||fQjCia|ss_^Tyu&5pi*GK0ZIT=YFh?6gGHXdWeaM zP0LNygM$qRx0|T2tkwA`8vM|GWiSV-g}*TQ^d6n3r)P&<_)4|?GP8d3trLvx?d=e8 z%0ZnhS(53$?qR3()Y6zVwvMx_*`7`{m{rHe#|qyD2Exiw!lI)&JTKhPwSc*U)Z9N@ z5ZOaTL-W~)4>r|EZcIdk3S4Ihg;;A4NoU-AnZ;QD(9k}3y3j--wXK5u{LsMO zrzP77uiG*cS#OrTST~@&$tCfT+`025GO~5J-$+9s@$7;o+vMkbtsO13#+QEtgMR**FPw9)M1E=EGtQd zG&L!Dh2z9d!GlbfT4lp)M9?+2Eaw_{{j?>|M%-gfZ4cgpd%gE?`=x>*+j*dxudpI{ z%c`a(Y$0sGLmY2X%gfokuz7iTJ4ngM$gbVE{fjr922*~LfXkkSWCu=LiqmMJBLZ}} z-?_QMZN0F2(6F&dIc?KZQf3pl-rg`$j$goK@$FHKuxaT1q zq%cbHcg7Ra-#Ps0BKX(bnOt23q|={GO-(cc{jGCzb8uHoOQEwVeI1hp;v5Q|&w%1_ z+#Og+*bjMLAWa(?y*gS!;;5Iu)He&AL62$K8RweD&^|c(IE{0!SfGi*H-{`DhpJG< zU^)$9#?|kz)cdWm@fU1`Jjn%c4&~*Okxn5Er^EXA(8x&Cl)T~puj^cD;I2$%vn1k= z5BnJ^!_T3ih^a$-T-?EAfsb$n#aL^R9CU3nWIt3@u5fey_>FbC@mA>eH{dK3cONwJHa=szozFQmPYekP*+_E0G z)0ykjWnF(ZVq|x{oaZIKlDc#UOQJv#<7Vxia;$%O(h?G z0J)YscV4^~ax5?bs{yhdV5_8V3B3QPNrO%dgzsp34zdh+!HDZv>+|!%?H^OI8FM7l z3biVOQg3r7WI6^vk%6K9=r_jw`}fgNIky3ZpT3n zho!tAgKVmyO_ij8`*5%ezeGP6K174;N#5k_#+ZYMgS1vzT*}s+8XWC}ujX3z1EsOm z-HS4a+}2Wi`&(IikG?Yt0CXMG^d?7TS60qTh?6sSu9Gy| z(BV4xGyx6QzeRso5W-Y6*$!)?A&d2D4x7{3!-1*S3`tZYo`wF#!$TvEjpwv$?dqG4z?4fDzI$t%#<#q16z^l6G8=a5t_9%`Kp0VF@#mb5_N> zHFGlp;t^kPIA`~ndy@FGs2;L`AsO37epYL`R{opoagMI^yo{}ViH>+c3l*2&vcD#! zsD47%{aPI=w)@MnpkOU_lW-|_@*8u|(n@G))qqA_t8-;IABse+PNwtjpIS;E#jF3H z=CHzLuxqbTU7ZwDqnM~@34VlQXu>ujhs~q9*NyM4fV>XEhlNTN4i1hleVegz@O)zwueijI~xXXmZ8AwGk8Z<*-; zG87-k5I>D+}*AynQMY$r`Y#*UvuWY|WQG%M)p3{X@r?DsD>hRf(@2CsEpZ=<0}$jKdUZAEk1J@)^I zRJRsg4ma}M=pUT2eSo6U@euX*%1Si^r&J^FGM5z>Kf^pZUQFt&-)Z9Sr4(cUPA*Iyra^ zVAyB^-T;1YvAq)fQlX5?X)8G`&EOx5-SzV)t?&l006^X7HI%PS4r)}Clb^;$M2MiZ zLf7~ZRayDg&CP9DH9Z@tz8{AY(z??n4TPq|q*#^cPJ+6Q(5ie=;}CG52g$pV#iuO5 z6jMy$ydZRDYHA9`HM+Cf@y%~b1K783r^lqf6(;oy48&Xq^x3|%&`I0AElal{RMm0r zn*8!BSa9Qn`7TFWdgmwm%irl37}C$&&yL>w1=|&LhllkKe2Y1@xBQyk=HYS8m)l8C zGiQxXsxLYR>6mGz)TtDEq3yqkuIk$oCErx0Vk23;efu_zoxFE6zi0hgk<2swrcY?N z0yH7=CdtZWo|hNd*)$#adDnI!#;RCt`{qr`%N(o1e|6Frzt8a9j`D`hz2CfD1<{~42+pC?&d02 z)^9LLD6FL3dH`LIghYm$)^)y+=l$+?dGQs{a_S5_YHD+&tLzNT&ACncDG%oI%eH|7 zfX?$V(#?S&;y^${#1s4A-~hJ+uz;*{i2f3EEo6ER_{KkMO`R$vXx`B*5_d00b{NA8 zE2CkX%TivpXISJpezhEnboYf``T0@YWvluB9y=Hk+As2L%-<{rPvc}Uxt0Pwe`muX zb#v>WeTO)N36}b;PvSSXo-4c)^1g=hjvaXK*6^dQD=6=(!FaAk<=_O#e8o%rrhM9+ zKTsNwcJv_eCvf|+9}AM|_os@m=v+k^@(Ld9kh&UhE9o0DCyw{|uYv6JgycoJ(*8X0 zLm4;-qRC6`o!KYuML3EP97P%9?@=TmXTIX|6n=s2b_mvs1RGn(j!&aQWV~D;8qTN^ z6@H5O&rhH5G++M}cs3VCoS1zTCHbyRBv$gu1*Y*In}fS3FDQ@)MJ*`13X;wT@teua ze~)hoX>;XwVGy>L=LVPl)(7NUl@pMYG_I1tWxim9D=w^rGzav4HtXs5kxQAsZ^kOP z&le@jfDee#1?(GuNEx=8n&Ut?DagrX6PurvxItk8XWC?}gsO1lD%cDl{vWU876OoI ziMt^AGXHwZ1cL9VR43I0-)oF9T{aCdZuHZ8p&s1TUP3V}cuF~Es z>3uUOdEWd3?5jZmYKklkc2b?fP%wr^0LH`nK$aa)>f-!7M4YuCb~-iX4uuUC7TdFD zv**e91ATqRdjoRNQ%Q-5yO1Y&2t>vFKRyuN-8 zF&)MCiX<(l(?n+UuCAxxa5CuBeoasR#C|?7D+v<;%Bk5_39dKYb)UQcVEyAacj_VI z>N!5T>z`TohyCHX*LD^EPW()rXJh5);OE20u$k(<(wK*}ikJxce)sK``=BpCC=g;m zvk>-h%8eWbHw}C-xtwlbsz~1%&n$O-ejYd?zw1H3$mR0irD>a!B*##}nqSIPaS@O5 z@kp#zMM}zzCFh$M(NE8l@}m0vuogcwF=b^2y2$jO#GxbAcdZpG&3x@~M<5dC0jdl< zT(EtC&RtwwC~xiUy=Dxkt2+lbFEurl=AbXMW3Xu?q6!d(7smbUr$$}Tpn5{%y^e~C zOo(d+1O`Gj$@XAP!wkvl0gq-B9!cV2RUWWayARtk4qz6V61 z-Ww4OIWM?{wEP1DGx+v4z#BI+G;A4BdAFV~P8rMbhP-YIWD4BoW3GKny4T#o8Otj2 z3N?~{7q||6Pn84|o)h03hY@cRTHOY^$WixtvBUP=uGuMc5(}raa7Wq1p`bY`F0SCS ztjN#%PPv*YQt3oUYZ+LXys~n$Ca%0~EWhUkSm%ahnLs|l@rKca_mF=Q6#M|;fQXz7 z*gAP1c}Ph~`S}~5ap&bR+`k_M=%n0YjG2KUphOe?2X=vB2Y6c`ek3-$kj2xHO7Z#7 ziL~d_#DbA1smjiW{1?bXv}KU0OnMtCgtoj|*Z_eXkE1QJIudN`VBjc#_;-dBH|q`` z*3|$O08FBIkU=HW_PfRZ*o+hRT{g94*z@S<=O-}6V9`QQ5UF*!JPt_ zphRY8V=Cu797o>#rX4kC|00$>mOV$MT(}yUAKK}PVaY?}rKit-#ic?+5E2md-JNP` z(g)28{Ci?v7gmxfcI#c$; zkvFd5vL06?1Cn9LpuypngX2GO)->x1J86efL~BF2BcM;ww!nnLwyYwg;aRD}eN4tO zr0$Og0{#T0zQ6YP^mJl$%kv6@X4&UIHXy~78T>PGA|N1uNysWc&5O!P{PO_DAdoV) zU5VYIteNsi^3}?;{yhXq6EU=#sEJ})Mj1I><00Fa_*zk^VWq_E>`{-|=N%uGr`>hz z6)ty05mfzseX)WQb3Yb5mDC7GT42T_=piqh`0(%F`erFqBGuq+L+w#W6D_v7y}*cm z9d2Mcv)4yhYzhJl_)2r&V`WPhW;_z7T?Y}3-u-<%qV50?AH8Z`G;AGE-_@lA1g>N} zqmviaV`OG+_Pp3l0$EnI4p=-)^klz?@h-pP(tvA^MZ&cQIf9}?sR4(rU*8}`ATc{m zZ%BeJ0m*~pn2>~>+t}C;FH4@veAJ=|D;yH-5I+!?Li9f$$=4FI_UorxmA^}91>zpm z(^&^v2TVE}ZaSGl7qT8`w32=nUCYt=ak1U23w>b)j3wo-RGj~{1Mj{NFcg;?DQ>K% zxBa^X17G#i5~MsBBX)mmOx2ifY9On>C+j+i}E_4ddkarb>@6(A~00a38}VSKxc$O1C5Bi^@*rRfKkROF(B;j zC=xin4^_PTU#}FO4O=UxG5h#e!Pc+<5Z`QOXZvsP<;dMD@g>z@{x__ zg-Q$l<8h`P5lOkku&l5MDXEoq1UkdVw7@6RPs(z4opRW%6~mzTf@TH$nCnB5LVvab zg|F}q;NLBm81TmBT_Gv%tnDWly%vuBy~M!EYWa3YFNSFjuqZ?qlvP0;D6!BNHdj|` zpeQdbnOwVeZQ(E)^jc7D=oJ~*`a3luSS?1swAMMS;ul8w1_v)@FhjhzY;Sirt(JZ> zx3E6-09KWxt7+ebd^kW*WHuG_XTdM|*FoLOl5@`Z=ek6H?E!SE6@dUGj=`IS;3)d% z^zI+f_Ft=rgyNjG@S7~oBj=WQllMf~9C9MX#b^a6aUxFHdTe20B+6Aa`1gG;M$OJu zYv07gX>g5t;yFvf?+4lrY!1BP6_`Wo>FHVIotN*J&G3W0+;^(~P2r)XJlRW4_3*@s z_2lNUv6yjMb}3g=5fKshy*@E0mIr%#XBVfNOfSE_kjS0fT&I&yv`Z}zK(!KsQcrP4 z4(FULd1#s$DXy$sSmH14)n8uiX;_3{t`jqLXke)O6jANw7BWPC*xTR>4`WR+4dOSs zbZL$3;(k+Hk#HwBGc*$>1*$FlZG?`;ax9zXR@N$e08fBA!Q-`}@(IS|^90lO6NEY;cZ zjN2p?4N$?LwiU6c!1zm+g_w_Xg(|WQ4a%B3pEGW6ZPC!urmW!*6BFOR|0DHAhGMaL z7gsiLXWwbHziIM-oof^7J}VU8z}*e8WBz7^Q2CeRCpo74 zR8h^ARmHWJuOi5XVAtAY@XsPLBDUXa`Fb3qiRm+RKhSXLZdzNqRF@K2J9AvlG+y{x z-no^xg&Qpi^-LRN^e9~q)A6!pAS%vb#9VYtP(;!BFODe8^=7d54^(UAzLZ0pxpZYh zko%>&dbUkZOm41u1jQ5t>rHabJZN#Y6$uWeAy)AJ*z#Ggr49YO0lFH_h{xgCRMfzk}&2rEZ81=zWeJZ z*(f}{JNuD0meY;-Nk^y+=YBB7ow=`2HW+@tsrBczi+!K#QQn4Gi;re_C+k5hB${z3 zJU+V#kyBIe^RES&juz>?c<}<_-H^>+>P?J{RE031%@);kv+7!8dLR&l6ulh+MX=DweUW}9#;is%NMW&!Y5q!mWtoZi59EJLyd@!mu4{lGakfCNfGo3 zi>Ye1p8@|}2pvs^ww#p9zOAPxSi35-JzZSbD?B_L5SXMC3@t&VZhCsUV)A_c#@x2nc;3(5TbhFBmz5gqj8fwvH4b()vZAu3RtuAuRL}6KAL=N;Jeml}r`&>4 zdrFW=z&+6QI9`BN0G<>LH$H6A=4JpYuH$8|OXLi!g+-n?rP*|?vyRU6c)2-07=$qE2&OALl%TmE!Ae}F!+C(6dic)w20+~3 z9kPWY%r29Q&#^rYePJXlc{1`bTEgoK1Zg4dBq z%#Ht-JLHKMK20;!rw)K8SwahP(+HK^BQv&d%fR-s<8?YpTN8Ch^Cu1VCM?1A=)}i@ z!GYGriz40Hw7#Q4+?|&TWxDedOcw%DJ10L3YgHj|e681nL!0_z00xTusmh!N7fPV_ zfCJDs0bs9mA724wCA<=X^=ltlDw88&MElP5tX25(IeaO91sU=glm#M9CxigXJboht zE1Axw8i=$Ro-o%t#3_3^7o{3x8sCXwUdw*(k_56AjKm?D;EmWC9n@j)xCY*6@J0@j zd*cK5c4$Igj1xWbr`X7!nmr%~GT-?35+wv1w#WY={&;_hf78Du{-4oa$jC3#QghV7 zng1pJvF}!kfP&}40nM-!;e3_v!I=?yA+J)N1b#Yw^*T3v$)o^DP^Y^nW-lj0m1-%o zS_s)eUqHEY19Ae>k0q^d*+9JD5Ayfi%U zkTfK3*XIxjLJE?WuK@Z>5kO?h%N>9o=0K}DTZqyvadb$T1PyrPj~m% ztv?OQE)wTomhi3D5pXABv<#VDsKLz4zqfF|$V#|wPE~+SlxrbC!~pgB_0h30;HRrl zSO4U%yc0jfR-ND@ym`|HyeJR_evOqPF0mmtgJ@5Xilb`%ovFRJ^m=|PbLZCDLG#SiP%Ez%&afS^8avcJR%}jxc_Z!ZP0Da%%k|R;hDf%OywKgg&@x5agS%XoEey3 zdEibu`kug~8-rFwS4W3mVeW%{jiOiI;HfM#CUL*D0$vBf_PN#hTS)|T$ccByUqGy2 zwdkLkT3FOVu^zvGk_RI`BQQR&On-6kxR45TYM@hTj6gv@{@LwhYDgdZwc63<{QzuD zF#g4b&LF+S8s_NenD?=^@}jS(PUV8d;Ga$ppj$v>9R=mm0%lH3kHs?+??4) z>vfIqURC_9B~ivu@l;PG_25k|MAWtalC}ch_L>)&oZESDF4^pAT^zOGddll8&R`OF4sgoM6>)CrVUp)7T+Nv$&_ zjbT4ss0K{?^=Qa>`}l~#j4+9bGxl;fDW97Y6#TMs&~`xfM%wZy2p%q0^WilZlL}We z=>*pTpe=a(qYa)7QBn6d$uj!8x)QjfuIfU#z|pb7-(Skc#s)NXQ``X+%zpcLQ(4~B$YdSTCnC94 z)3p+tze{`g;-2aWdU$w1J`nb2B9F6RAq>8du_f|*dO&Ml^Gfe1bcfh)!^ui644KG5 z4j=Z&U}|BZ+yIBmo=GhG_^nmZ(L1>Krj{_NQ_gBN=>$)W^%`q&_^x^--0m-a7hSM; z8`ra0>lQ8+)CRFowt6C;4X;tmWILdo%K_!Lw zRS|sL!nFsht-Ws&^uO39-s|q{^cNI@4pe{2{IjlmWWQr<^q7o{>omyhAv>WLJr*WSN> zAKtItPeu3hw#ZGwaAtxCS^|slGGFv?657X#kJFgz96`V-JBKY)xQA+sI2eZ#rX(7} z>-rpn_|r5-9R(fDk6VQLP(m1bLLg@aqk`V($kaSz`g1o{=_*g7znnxlRtE*;i(b|@ zW*-Nsq~A|M#c%%(mAbuG;%zTG-oWvFAYz~FYE8TASe{3JeG3|YVI668UR&#=)z}k_ zM2aEy0vk5m<)vgD7?Oe+Ntlayhb0R;^YuDhdRE4cf+J)S%U+u=z{f#EAwHo#=1ML> z+$NHphUr<89G*#7u_m;gTsT<*>^FDwAwfSRXJfoL1}xS6U5H@rk(WW?$nLASR(Dre3-X~io*CKJM1;^0h zfQ%18ieh|6Ar-ggtFuzTaRgr)!p`2 zROs3YHei?uR1DGM3Z4_M|N0*v}AFleH zp>c}pEy4b|GoEXE`Ogk|dNr$Zvf<2c{0hoNLVqT<2iM#1mTv=&^dFp+oOK3dw3JPZ zWA;5pj7c#W`-V(8tO95Eh3gSlR|wRikpu4s zvi+L!5o+9%`~`=c4wf%>$^^#q1Wg`+LO$3{uZYWJGA4jVqMtK0I;uV{MbFG^3-iAJ zs1mn94|`e$chk0kn9jh+NOeO?HMzfQ5Jt@Ez=KPI)lNfW0%8_0kbKSZlk>wF!?#E6 z_jY)uk;QnygY~~Hawo>p&$^~DBlT#xs`G49lvFOk{dpC{#mA4se^2dhpF6p>46+nF z9eS_~)4|K~%@D=a14dx)WboFr^R>AW zf?$voN1{-t4jlFKodv0BA(8Ugw46^s6TXs^%Q2xeHpE-G;4}I&9yP?%lNZIs>~R+f z7K9<*PIe6S*4g7#d0y7T;%+in5r@{FlQ6tf9Shz5-Sdg+^553Ew7TvECO_yxVY&`T zaIpi365&$7R65mYB*ev&vHqD_AKdE7He2dVfwA3iZ4l((IDrBsjxU5c1XTXp zDFL_31%=B5uTZMk?meId!I(4`o&=WEqI$N%DB5%9;r{t%Ywx?Gf%28cr9Qio1P9!& zaMBts`8C$(RKIBj*{>OYtF5ckQ27ILXu2Vc%I4PNlU(lY?Kx%WueqY&VNwTUGG5kV zBLjuUI}334(wP^6A-P!H$txQ$S?!QpR3v}=zL9yYdN*AnOkKqY99al1#DFWGi!e7r zpc!Jg#Gx;~gik@pC0>R-uY?9W9*m2lJ>aXL=RY^y>8LtEB=}xOIl9?qKE+{~Db{Mz zVjawPS+sZg0F!B2Pv!lT_;8U`4E-UWxpgw>YkpPE5!)0g?o>s zZS3rnzJB!c&a^yO88U~z1*JT%7}9sT?)yU!&l;Q4F%|x+OTY&rP!GR%@i-VW#(jd! z#b}22MM6qzxe*)_w|^N>hoaN}1Yl1wzQa#xHl#vgx(GeJ`ykMMx!(|S0eLS`|K%b6 z)imH!j7%jyYHICz?kem6{k%(m4drYiS}dH8iqU^1Q7diXq$pK_CW_JQlv zKqIZus(F5~zi^3H=UK{V_zY_JM9x^4VFvXh2CPJVMqzlu0y6w&>KXu3E3Br>tgK|k z?77CrCMLAh)qBm2yQ0Z1i32;>!OJE^IswJ}e0+SoyxNWhQX0U>A)L$oSCWwELx6Cj zs;cULpD9y_DR7nYP`e4T8rQF1?;W3i z6%q^?iI80avH7Kl^>vhAN$Z6@t?6=f?)u>y1JY9s%;<#UlG3*!UcG*Q{VDI85M)cm z(~71f*<&<$m7AcJ?0NYHhJgfY`dJ(!zx7q<*6#?@?FVV0TJ!hcMW+V^!Op=U!)6zZ z2f%?AGxg^H5Z%fL&DlBhn?Ix7d&~+s0wyMF$csdrAzUE-VbmQz2J1#r3MszWJHYg! zt=(-Z#&IKvx-LJJxCyboPtc(uw$$P2RjS+IVc%#qR+5pDLWb!AAHfCUe$vry5n zADp?v&+>QEUCoM$j)_qSPcIXr=+AWAoF=(@SK*lG3d)gj=flzE;|G;dj6{;R_h_CI zT-uh(+5`wR(TmBE7ikse{FepZ^68(m?=AQ1$js-@y8rKpPtRX-a^>rqtukNxR<8o? z6aX#~U~F=iyQcK fx)n>eZ24#ZcZ1BowK1Faf)seV`njxgN@xNATN@Pc literal 0 HcmV?d00001 From 78bc7b81567209a660f455e342c9295613f1af88 Mon Sep 17 00:00:00 2001 From: Shiny <36184001+ShinyNT@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:43:58 -0300 Subject: [PATCH 006/112] Docs: update Pokemon R/B spanish guide (#2672) * Update setup_es.md * Update setup_es.md i'm stupid and actually didn't edit the client chose part lol --- worlds/pokemon_rb/docs/setup_es.md | 49 +++++++++++++++++++----------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md index 6499c9501263..67024c5b52ec 100644 --- a/worlds/pokemon_rb/docs/setup_es.md +++ b/worlds/pokemon_rb/docs/setup_es.md @@ -11,8 +11,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux - Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba. - Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se encuentra en el enlace de arriba. -- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases) - (selecciona `Pokemon Client` durante la instalación). +- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases). - Los ROMs originales de Pokémon Red y/o Blue. La comunidad de Archipelago no puede proveerlos. ## Software Opcional @@ -75,27 +74,41 @@ Y los siguientes caracteres especiales (cada uno ocupa un carácter): ## Unirse a un juego MultiWorld -### Obtener tu parche de Pokémon +### Generar y parchar un juego -Cuando te unes a un juego multiworld, se te pedirá que entregues tu archivo YAML a quien lo esté organizando. -Una vez que la generación acabe, el anfitrión te dará un enlace a tu archivo, o un .zip con los archivos de -todos. Tu archivo tiene una extensión `.apred` o `.apblue`. +1. Crea tu archivo de opciones (YAML). +2. Sigue las instrucciones generales de Archipelago para [generar un juego](../../Archipelago/setup/en#generating-a-game). +Haciendo esto se generará un archivo de salida. Tu parche tendrá la extensión de archivo `.apred` o `.apblue`. +3. Abre `ArchipelagoLauncher.exe` +4. Selecciona "Open Patch" en el lado izquierdo y selecciona tu parche. +5. Si es tu primera vez parchando, se te pedirá que selecciones tu ROM original. +6. Un archivo `.gb` parchado será creado en el mismo lugar donde está el parche. +7. La primera vez que abras un parche con BizHawk Client, también se te pedira ubicar `EmuHawk.exe` en tu +instalación de BizHawk. -Haz doble clic en tu archivo `.apred` o `.apblue` para que se ejecute el cliente y realice el parcheado del ROM. -Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirán automáticamente (si es que se -ha asociado la extensión al emulador tal como fue recomendado) +Si estás jugando una semilla single-player y no te importa tener seguimiento ni pistas, puedes terminar aqui, cerrar el +cliente, y cargar el ROM parchado en cualquier emulador. Sin embargo, para multiworlds y otras funciones de Archipelago, +continúa con los pasos abajo, usando el emulador BizHawk. ### Conectarse al multiserver -Una vez ejecutado tanto el cliente como el emulador, hay que conectarlos. Abre la carpeta de instalación de Archipelago, -luego abre `data/lua`, y simplemente arrastra el archivo `connector_pkmn_rb.lua` a la ventana principal de Emuhawk. -(Alternativamente, puedes abrir la consola de Lua manualmente. En Emuhawk ir a Tools > Lua Console, luego ir al menú -`Script` 〉 `Open Script`, navegar a la ubicación de `connector_pkmn_rb.lua` y seleccionarlo.) - -Para conectar el cliente con el servidor, simplemente pon `:` en la caja de texto superior y presiona -enter (si el servidor tiene contraseña, en la caja de texto inferior escribir `/connect : [contraseña]`) - -Ahora ya estás listo para tu aventura en Kanto. +Por defecto, abrir un parche hará los pasos del 1 al 5 automáticamente. Incluso asi, es bueno memorizarlos en caso de +que tengas que cerrar y volver a abrir el juego por alguna razón. + +1. Pokémon Red/Blue usa el BizHawk Client de Archipelago. Si el cliente no está abierto desde cuando parchaste tu juego, +puedes volverlo a abrir desde el Launcher. +2. Asegúrate que EmuHawk esta cargando el ROM parchado. +3. En EmuHawk, ir a `Tools > Lua Console`. Esta ventana debe quedarse abierta mientras se juega. +4. En la ventana de Lua Console, ir a `Script > Open Script…`. +5. Navegar a tu carpeta de instalación de Archipelago y abrir `data/lua/connector_bizhawk_generic.lua`. +6. El emulador se puede congelar por unos segundos hasta que logre conectarse al cliente. Esto es normal. La ventana del +BizHawk Client debería indicar que se logro conectar y reconocer Pokémon Red/Blue. +7. Para conectar el cliente al servidor, ingresa la dirección y el puerto (por ejemplo, `archipelago.gg:38281`) en el +campo de texto superior del cliente y y haz clic en Connect. + +Para conectar el cliente al multiserver simplemente escribe `:` en el campo de texto superior y +presiona enter (si el servidor usa contraseña, escribe en el campo de texto inferior +`/connect :[contraseña]`) ## Auto-Tracking From 283d1ab7e8d88a63f47bf1a2e82fd13ea56a889f Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:35:09 -0500 Subject: [PATCH 007/112] DLC Quest Bug Fix 50+ coin bundle basic Campaign (#4276) * DLC Quest Bug Fix * DLC Quest Bug Fix --- worlds/dlcquest/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index b8f2aad6ff94..37eae9b447d1 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -72,8 +72,16 @@ def create_items(self): self.multiworld.itempool += created_items - if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both: - self.multiworld.early_items[self.player]["Movement Pack"] = 1 + campaign = self.options.campaign + has_both = campaign == Options.Campaign.option_both + has_base = campaign == Options.Campaign.option_basic or has_both + has_big_bundles = self.options.coinsanity and self.options.coinbundlequantity > 50 + early_items = self.multiworld.early_items + if has_base: + if has_both and has_big_bundles: + early_items[self.player]["Incredibly Important Pack"] = 1 + else: + early_items[self.player]["Movement Pack"] = 1 for item in items_to_exclude: if item in self.multiworld.itempool: @@ -82,7 +90,7 @@ def create_items(self): def precollect_coinsanity(self): if self.options.campaign == Options.Campaign.option_basic: if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5: - self.multiworld.push_precollected(self.create_item("Movement Pack")) + self.multiworld.push_precollected(self.create_item("DLC Quest: Coin Bundle")) def create_item(self, item: Union[str, ItemData], classification: ItemClassification = None) -> DLCQuestItem: if isinstance(item, str): From 3ba0576cf6a1c96452b4babef67eedca18f4f9b3 Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:36:21 -0700 Subject: [PATCH 008/112] CV64: Fix the first Waterway 3HB ledge setting the flag of one of the Nitro room item locations. #4277 --- worlds/cv64/rom.py | 63 +++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py index db621c7101d6..7af4e3807ac2 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -684,38 +684,37 @@ def apply_patches(caller: APProcedurePatch, rom: bytes, options_file: str) -> by # Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and # setting flags instead. - if options["multi_hit_breakables"]: - rom_data.write_int32(0xE87F8, 0x00000000) # NOP - rom_data.write_int16(0xE836C, 0x1000) - rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34 - rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter) - # Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one) - rom_data.write_int32(0xE7D54, 0x00000000) # NOP - rom_data.write_int16(0xE7908, 0x1000) - rom_data.write_byte(0xE7A5C, 0x10) - rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C - rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter) - - # New flag values to put in each 3HB vanilla flag's spot - rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock - rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock - rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub - rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab - rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab - rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock - rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge - rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge - rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate - rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal - rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab - rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge - rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate - rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab - rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab - rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab - rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab - rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier - rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data + rom_data.write_int32(0xE87F8, 0x00000000) # NOP + rom_data.write_int16(0xE836C, 0x1000) + rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34 + rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter) + # Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one) + rom_data.write_int32(0xE7D54, 0x00000000) # NOP + rom_data.write_int16(0xE7908, 0x1000) + rom_data.write_byte(0xE7A5C, 0x10) + rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C + rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter) + + # New flag values to put in each 3HB vanilla flag's spot + rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock + rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock + rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub + rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab + rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab + rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock + rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge + rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge + rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate + rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal + rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab + rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge + rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate + rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab + rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab + rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab + rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab + rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier + rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data # Once-per-frame gameplay checks rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034 From 4780fd9974a9392b009e17c72b8f8a1f7671698f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:37:19 +0100 Subject: [PATCH 009/112] The Witness: Rename some *horrendously* named variables (#4258) * Rename all instances of 'multi' to 'progressive' and all instances of 'prog' to 'progression' * We do a little reordering * More * One more --- worlds/witness/hints.py | 34 +++++++++++++++----------- worlds/witness/player_items.py | 4 ++-- worlds/witness/player_logic.py | 44 ++++++++++++++++++---------------- worlds/witness/rules.py | 6 ++--- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index dac7e3fb4d05..82837aed0686 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -301,11 +301,11 @@ def hint_from_location(world: "WitnessWorld", location: str) -> Optional[Witness def get_item_and_location_names_in_random_order(world: "WitnessWorld", own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]: - prog_item_names_in_this_world = [ + progression_item_names_in_this_world = [ item.name for item in own_itempool if item.advancement and item.code and item.location ] - world.random.shuffle(prog_item_names_in_this_world) + world.random.shuffle(progression_item_names_in_this_world) locations_in_this_world = [ location for location in world.multiworld.get_locations(world.player) @@ -318,22 +318,24 @@ def get_item_and_location_names_in_random_order(world: "WitnessWorld", location_names_in_this_world = [location.name for location in locations_in_this_world] - return prog_item_names_in_this_world, location_names_in_this_world + return progression_item_names_in_this_world, location_names_in_this_world def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location] ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]: - prog_items_in_this_world, loc_in_this_world = get_item_and_location_names_in_random_order(world, own_itempool) + progression_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order( + world, own_itempool + ) always_items = [ item for item in get_always_hint_items(world) - if item in prog_items_in_this_world + if item in progression_items_in_this_world ] priority_items = [ item for item in get_priority_hint_items(world) - if item in prog_items_in_this_world + if item in progression_items_in_this_world ] if world.options.vague_hints: @@ -341,11 +343,11 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["Wi else: always_locations = [ location for location in get_always_hint_locations(world) - if location in loc_in_this_world + if location in locations_in_this_world ] priority_locations = [ location for location in get_priority_hint_locations(world) - if location in loc_in_this_world + if location in locations_in_this_world ] # Get always and priority location/item hints @@ -376,7 +378,9 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["Wi def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint], unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]: - prog_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order(world, own_itempool) + progression_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order( + world, own_itempool + ) next_random_hint_is_location = world.random.randrange(0, 2) @@ -390,7 +394,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp } while len(hints) < hint_amount: - if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first: + if not progression_items_in_this_world and not locations_in_this_world and not hints_to_use_first: logging.warning(f"Ran out of items/locations to hint for player {world.player_name}.") break @@ -399,8 +403,8 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp location_hint = hints_to_use_first.pop() elif next_random_hint_is_location and locations_in_this_world: location_hint = hint_from_location(world, locations_in_this_world.pop()) - elif not next_random_hint_is_location and prog_items_in_this_world: - location_hint = hint_from_item(world, prog_items_in_this_world.pop(), own_itempool) + elif not next_random_hint_is_location and progression_items_in_this_world: + location_hint = hint_from_item(world, progression_items_in_this_world.pop(), own_itempool) # The list that the hint was supposed to be taken from was empty. # Try the other list, which has to still have something, as otherwise, all lists would be empty, # which would have triggered the guard condition above. @@ -587,9 +591,11 @@ def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations hints = [] for hinted_area in hinted_areas: - hint_string, prog_amount, hunt_panels = word_area_hint(world, hinted_area, items_per_area[hinted_area]) + hint_string, progression_amount, hunt_panels = word_area_hint(world, hinted_area, items_per_area[hinted_area]) - hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount, hunt_panels)) + hints.append( + WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", progression_amount, hunt_panels) + ) if len(hinted_areas) < amount: logging.warning(f"Was not able to make {amount} area hints for player {world.player_name}. " diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 3be298ebccae..831e614f21c4 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -55,7 +55,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, name: data for (name, data) in self.item_data.items() if data.classification not in {ItemClassification.progression, ItemClassification.progression_skip_balancing} - or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME + or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME } # Downgrade door items @@ -76,7 +76,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, } for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): - num_progression = len(self._logic.MULTI_LISTS[item_name]) + num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name]) self._mandatory_items[item_name] = num_progression else: self._mandatory_items[item_name] = 1 diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index f8b7db3570a9..fd86679844a7 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -75,13 +75,15 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.UNREACHABLE_REGIONS: Set[str] = set() + self.THEORETICAL_BASE_ITEMS: Set[str] = set() self.THEORETICAL_ITEMS: Set[str] = set() - self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set() - self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1) - self.MULTI_LISTS: Dict[str, List[str]] = {} - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + + self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1) + self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {} self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} + self.STARTING_INVENTORY: Set[str] = set() self.DIFFICULTY = world.options.puzzle_randomization @@ -183,13 +185,13 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off) these_items = frozenset({ - subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) + subset.intersection(self.THEORETICAL_BASE_ITEMS) for subset in these_items }) # Update the list of "items that are actually being used by any entity" for subset in these_items: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(subset) # Handle door entities (door shuffle) if entity_hex in self.DOOR_ITEMS_BY_ID: @@ -197,7 +199,7 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) for dependent_item in door_items: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(dependent_item) these_items = logical_and_witness_rules([door_items, these_items]) @@ -299,10 +301,10 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: self.THEORETICAL_ITEMS.add(item_name) if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition, - static_witness_logic.ALL_ITEMS[item_name]).child_item_names) + self.THEORETICAL_BASE_ITEMS.update(cast(ProgressiveItemDefinition, + static_witness_logic.ALL_ITEMS[item_name]).child_item_names) else: - self.THEORETICAL_ITEMS_NO_MULTI.add(item_name) + self.THEORETICAL_BASE_ITEMS.add(item_name) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -316,11 +318,11 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: self.THEORETICAL_ITEMS.discard(item_name) if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_ITEMS_NO_MULTI.difference_update( + self.THEORETICAL_BASE_ITEMS.difference_update( cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names ) else: - self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name) + self.THEORETICAL_BASE_ITEMS.discard(item_name) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -843,7 +845,7 @@ def make_dependency_reduced_checklist(self) -> None: self.REQUIREMENTS_BY_HEX = {} self.USED_EVENT_NAMES_BY_HEX = defaultdict(list) self.CONNECTIONS_BY_REGION_NAME = {} - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME = set() # Make independent requirements for entities for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): @@ -868,18 +870,18 @@ def finalize_items(self) -> None: """ Finalise which items are used in the world, and handle their progressive versions. """ - for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: + for item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: if item not in self.THEORETICAL_ITEMS: progressive_item_name = static_witness_logic.get_parent_progressive_item(item) - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) child_items = cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names - multi_list = [child_item for child_item in child_items - if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] - self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1 - self.MULTI_LISTS[progressive_item_name] = multi_list + progressive_list = [child_item for child_item in child_items + if child_item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME] + self.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] = progressive_list.index(item) + 1 + self.PROGRESSIVE_LISTS[progressive_item_name] = progressive_list else: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(item) def solvability_guaranteed(self, entity_hex: str) -> bool: return not ( diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 74ea2aef5740..323d5943c853 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -201,10 +201,10 @@ def _has_item(item: str, world: "WitnessWorld", if item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) - prog_item = static_witness_logic.get_parent_progressive_item(item) - needed_amount = player_logic.MULTI_AMOUNTS[item] + actual_item = static_witness_logic.get_parent_progressive_item(item) + needed_amount = player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] - simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(prog_item, needed_amount) + simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(actual_item, needed_amount) return simple_rule From 5d30d16e09d981eb3263c683c742300af1d4d81c Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:37:33 +0100 Subject: [PATCH 010/112] Docs: Mention explicit_indirect_conditions & "Menu" -> origin_region_name (#3887) * Docs: Mention explicit_indirect_conditions https://github.com/ArchipelagoMW/Archipelago/pull/3682 * Update world api.md * Docs: "Menu" -> origin_region_name https://github.com/ArchipelagoMW/Archipelago/pull/3682 * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update world api.md * I just didn't do this one and then Medic approved it anyway LMAO * Update world api.md --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- docs/world api.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index bf09d965f11d..20669d7ae7be 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -288,8 +288,8 @@ like entrance randomization in logic. Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions. -There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to -return to the "Menu" region by resetting the game ("Save and quit"). +There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)), +from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit"). ### Entrances @@ -328,6 +328,9 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance. You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case. +Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301), +avoiding the need for indirect conditions at the expense of performance. + ### Item Rules An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to @@ -463,7 +466,7 @@ The world has to provide the following things for generation: * the properties mentioned above * additions to the item pool -* additions to the regions list: at least one called "Menu" +* additions to the regions list: at least one named after the world class's origin_region_name ("Menu" by default) * locations placed inside those regions * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand * applying `self.multiworld.push_precollected` for world-defined start inventory @@ -516,7 +519,7 @@ def generate_early(self) -> None: ```python def create_regions(self) -> None: - # Add regions to the multiworld. "Menu" is the required starting point. + # Add regions to the multiworld. One of them must use the origin_region_name as its name ("Menu" by default). # Arguments to Region() are name, player, multiworld, and optionally hint_text menu_region = Region("Menu", self.player, self.multiworld) self.multiworld.regions.append(menu_region) # or use += [menu_region...] From 3cb5219e09a4908cfbce3249372bffff9ca32410 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 29 Nov 2024 00:38:17 +0000 Subject: [PATCH 011/112] Core: Fix playthrough only checking half of the sphere 0 items (#4268) * Core: Fix playthrough only checking half of the sphere 0 items The lists of precollected items were being mutated while iterating those same lists, causing playthrough to skip checking half of the sphere 0 advancement items. This patch ensures the lists are copied before they are iterated. * Replace chain.from_iterable with two for loops for better clarity Added a comment to `multiworld.push_precollected(item)` to explain that it is also modifying `precollected_items`. --- BaseClasses.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 2e4efd606df9..1ee27e02fe54 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1384,14 +1384,21 @@ def create_playthrough(self, create_paths: bool = True) -> None: # second phase, sphere 0 removed_precollected: List[Item] = [] - for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): - logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) - multiworld.precollected_items[item.player].remove(item) - multiworld.state.remove(item) - if not multiworld.can_beat_game(): - multiworld.push_precollected(item) - else: - removed_precollected.append(item) + + for precollected_items in multiworld.precollected_items.values(): + # The list of items is mutated by removing one item at a time to determine if each item is required to beat + # the game, and re-adding that item if it was required, so a copy needs to be made before iterating. + for item in precollected_items.copy(): + if not item.advancement: + continue + logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) + precollected_items.remove(item) + multiworld.state.remove(item) + if not multiworld.can_beat_game(): + # Add the item back into `precollected_items` and collect it into `multiworld.state`. + multiworld.push_precollected(item) + else: + removed_precollected.append(item) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others From c022c742b529fe2dda59c2606a590c35c195e2cc Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:38:53 -0500 Subject: [PATCH 012/112] Core: Add item.filler helper (#4081) * Add filler helper * Update BaseClasses.py --- BaseClasses.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index 1ee27e02fe54..9bf9e68608a5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1262,6 +1262,10 @@ def useful(self) -> bool: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def filler(self) -> bool: + return not (self.advancement or self.useful or self.trap) + @property def excludable(self) -> bool: return not (self.advancement or self.useful) From ce78c75999aaa1573f1d8ab16b6e190f163ede2e Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:40:53 -0500 Subject: [PATCH 013/112] OoT: Turn Logic Tricks into an OptionSet (#3551) * Alphabetizing WebHost display for logic tricks * Convert to a Set * Changing this back to match upstream --- worlds/oot/Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 613c5d01b381..797b276b766c 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,7 +1,7 @@ import typing import random from dataclasses import dataclass -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \ +from Options import Option, DefaultOnToggle, Toggle, Range, OptionSet, DeathLink, PlandoConnections, \ PerGameCommonOptions, OptionGroup from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks @@ -1272,7 +1272,7 @@ class SfxOcarina(Choice): } -class LogicTricks(OptionList): +class LogicTricks(OptionSet): """Set various tricks for logic in Ocarina of Time. Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"]. A full list of supported tricks can be found at: From 62e4285924f5825f87ad141562c5a7f8bbb3fc34 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:41:13 +0100 Subject: [PATCH 014/112] Core: Make region.add_exits return the created Entrances (#3885) * Core: Make region.add_exits return the created Entrances * Update BaseClasses.py * Update BaseClasses.py Co-authored-by: Doug Hoskisson --------- Co-authored-by: Doug Hoskisson --- BaseClasses.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9bf9e68608a5..700a21506ac8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1110,7 +1110,7 @@ def create_exit(self, name: str) -> Entrance: return exit_ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], - rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None: + rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]: """ Connects current region to regions in exit dictionary. Passed region names must exist first. @@ -1120,10 +1120,14 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], """ if not isinstance(exits, Dict): exits = dict.fromkeys(exits) - for connecting_region, name in exits.items(): - self.connect(self.multiworld.get_region(connecting_region, self.player), - name, - rules[connecting_region] if rules and connecting_region in rules else None) + return [ + self.connect( + self.multiworld.get_region(connecting_region, self.player), + name, + rules[connecting_region] if rules and connecting_region in rules else None, + ) + for connecting_region, name in exits.items() + ] def __repr__(self): return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' From 82260d728fb5780dfba12e93c53b840be48d9299 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:41:40 +0100 Subject: [PATCH 015/112] The Witness: Add Fast Travel Option (#3766) * add unlockable warps * Change Swamp Near Platform to Swamp Platform * apply changes to variety as well --- worlds/witness/data/WitnessLogic.txt | 4 ++-- worlds/witness/data/WitnessLogicExpert.txt | 4 ++-- worlds/witness/data/WitnessLogicVanilla.txt | 4 ++-- worlds/witness/data/WitnessLogicVariety.txt | 4 ++-- worlds/witness/options.py | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index 8fadf68c3131..0dbb88a107b1 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609: 158302 - 0x00609 (Sliding Bridge) - True - Shapers 159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True 159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True -Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: 158313 - 0x00982 (Platform Row 1) - True - Shapers 158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers 158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index c6d6efa96485..0f601724acbe 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers & Rotated Shapers & Triangles & Black/White Squares 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Rotated Shapers & Triangles & Black/White Squares -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609: 158302 - 0x00609 (Sliding Bridge) - True - Shapers & Black/White Squares 159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True 159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True -Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: 158313 - 0x00982 (Platform Row 1) - True - Rotated Shapers 158314 - 0x0097F (Platform Row 2) - 0x00982 - Rotated Shapers 158315 - 0x0098F (Platform Row 3) - 0x0097F - Rotated Shapers diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 1186c470233e..f0c6a8690ed3 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609: 158302 - 0x00609 (Sliding Bridge) - True - Shapers 159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True 159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True -Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: 158313 - 0x00982 (Platform Row 1) - True - Shapers 158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers 158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt index 31263aa33790..b7b705a6db9f 100644 --- a/worlds/witness/data/WitnessLogicVariety.txt +++ b/worlds/witness/data/WitnessLogicVariety.txt @@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Rotated Shapers & Triangles 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers & Triangles -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609: 158302 - 0x00609 (Sliding Bridge) - True - Shapers 159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True 159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True -Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: 158313 - 0x00982 (Platform Row 1) - True - Shapers 158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers 158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers diff --git a/worlds/witness/options.py b/worlds/witness/options.py index d1713e73c541..bb935388e3c7 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -164,6 +164,16 @@ class ObeliskKeys(DefaultOnToggle): display_name = "Obelisk Keys" +class UnlockableWarps(Toggle): + """ + Adds unlockable fast travel points to the game. + These warp points are represented by spheres in game. You walk up to one, you unlock it for warping. + + The warp points are: Entry, Symmetry Island, Desert, Quarry, Keep, Shipwreck, Town, Jungle, Bunker, Treehouse, Mountaintop, Caves. + """ + display_name = "Unlockable Fast Travel Points" + + class ShufflePostgame(Toggle): """ Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal. @@ -424,6 +434,7 @@ class TheWitnessOptions(PerGameCommonOptions): shuffle_discarded_panels: ShuffleDiscardedPanels shuffle_vault_boxes: ShuffleVaultBoxes obelisk_keys: ObeliskKeys + unlockable_warps: UnlockableWarps shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815 EP_difficulty: EnvironmentalPuzzlesDifficulty shuffle_postgame: ShufflePostgame @@ -479,6 +490,9 @@ class TheWitnessOptions(PerGameCommonOptions): ShuffleBoat, ObeliskKeys, ]), + OptionGroup("Warps", [ + UnlockableWarps, + ]), OptionGroup("Filler Items", [ PuzzleSkipAmount, TrapPercentage, From 710cf4ebba67198133bd02150b08e757c3fc9c0d Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:42:08 +0100 Subject: [PATCH 016/112] Core: Add __iter__ to VerifyKeys (#3550) * Add __iter__ to VerifyKeys * Typing --- Options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Options.py b/Options.py index 992348cb546d..d81f81face06 100644 --- a/Options.py +++ b/Options.py @@ -828,7 +828,10 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P f"is not a valid location name from {world.game}. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + def __iter__(self) -> typing.Iterator[typing.Any]: + return self.value.__iter__() + class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): default = {} supports_weighting = False From 1ba7700283a50e3b11ae791452cc6b80d0d3fe26 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Fri, 29 Nov 2024 10:44:21 +1000 Subject: [PATCH 017/112] Muse Dash: Change AttributeError to KeyError when Create_Item receives an item name that doesn't exist in the world (#4215) * Change missing attribute error to key error. * Swap to explicit key error * Revert "Swap to explicit key error" This reverts commit 719255891e543d15720b6bdeb293621266618efc. --- worlds/musedash/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index ab3a4819fc48..be2eec2f87b8 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -183,7 +183,7 @@ def create_item(self, name: str) -> Item: if album: return MuseDashSongItem(name, self.player, album) - song = self.md_collection.song_items.get(name) + song = self.md_collection.song_items[name] return MuseDashSongItem(name, self.player, song) def get_filler_item_name(self) -> str: From faeb54224edeeba14a2491acb10e81d5920ead32 Mon Sep 17 00:00:00 2001 From: josephwhite Date: Thu, 28 Nov 2024 19:45:26 -0500 Subject: [PATCH 018/112] Super Mario 64: Option groups (#4161) * sm64ex: add option groups * sm64ex: rename sanity options group to item options * sm64ex: rename sanity options group to logic options * sm64ex: seperate star costs from goal options and add entrance rando to logic options * sm64ex: seperate ability options from logic options group --- worlds/sm64ex/Options.py | 28 +++++++++++++++++++++++++++- worlds/sm64ex/__init__.py | 4 +++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 8269d3a262cd..6cf233558ce2 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,6 +1,6 @@ import typing from dataclasses import dataclass -from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet +from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet, OptionGroup from .Items import action_item_table class EnableCoinStars(DefaultOnToggle): @@ -127,6 +127,32 @@ class MoveRandomizerActions(OptionSet): valid_keys = [action for action in action_item_table if action != 'Double Jump'] default = valid_keys +sm64_options_groups = [ + OptionGroup("Logic Options", [ + AreaRandomizer, + BuddyChecks, + ExclamationBoxes, + ProgressiveKeys, + EnableCoinStars, + StrictCapRequirements, + StrictCannonRequirements, + ]), + OptionGroup("Ability Options", [ + EnableMoveRandomizer, + MoveRandomizerActions, + StrictMoveRequirements, + ]), + OptionGroup("Star Options", [ + AmountOfStars, + FirstBowserStarDoorCost, + BasementStarDoorCost, + SecondFloorStarDoorCost, + MIPS1Cost, + MIPS2Cost, + StarsToFinish, + ]), +] + @dataclass class SM64Options(PerGameCommonOptions): area_rando: AreaRandomizer diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index d4bafbafcc57..40c778ebe66c 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -3,7 +3,7 @@ import json from .Items import item_table, action_item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location -from .Options import SM64Options +from .Options import sm64_options_groups, SM64Options from .Rules import set_rules from .Regions import create_regions, sm64_level_to_entrances, SM64Levels from BaseClasses import Item, Tutorial, ItemClassification, Region @@ -20,6 +20,8 @@ class SM64Web(WebWorld): ["N00byKing"] )] + option_groups = sm64_options_groups + class SM64World(World): """ From b972e8c0716dc39661b055ebc289d2b9b6d7d34f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 29 Nov 2024 01:57:18 +0100 Subject: [PATCH 019/112] Core: fix deprecation warning for utcnow() in setup.py (#4170) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f075551d58b0..59c2d698d35b 100644 --- a/setup.py +++ b/setup.py @@ -321,7 +321,7 @@ def run(self) -> None: f"{ex}\nPlease close all AP instances and delete manually.") # regular cx build - self.buildtime = datetime.datetime.utcnow() + self.buildtime = datetime.datetime.now(datetime.timezone.utc) super().run() # manually copy built modules to lib folder. cx_Freeze does not know they exist. From b783eab1e868160a14e4a424abdb105c11004eea Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:10:31 -0500 Subject: [PATCH 020/112] Core: Introduce 'Hint Priority' concept (#3506) * Introduce 'Hint Priority' concept * fix error when sorting hints while not connected * fix 'found' -> 'status' kivy stuff * remove extraneous warning this warning fired if you clicked to select or toggle priority of any hint, as you weren't clicking on the header... * skip scanning individual header widgets when not clicking on the header * update hints on disconnection * minor cleanup * minor fixes/cleanup * fix: hints not updating properly for receiving player * update re: review * 'type() is' -> 'isinstance()' * cleanup, re: Jouramie's review * Change 'priority' to 'status', add 'Unspecified' and 'Avoid' statuses, update colors * cleanup * move dicts out of functions * fix: new hints being returned when hint already exists * fix: show `Found` properly when hinting already-found hints * import `Hint` and `HintStatus` directly from `NetUtils` * Default any hinted `Trap` item to be classified as `Avoid` by default * add some sanity checks * re: Vi's feedback * move dict out of function * Update kvui.py * remove unneeded dismiss message * allow lclick to drop hint status dropdown * underline hint statuses to indicate clickability * only underline clickable statuses * Update kvui.py * Update kvui.py --------- Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 12 ++- Main.py | 2 +- MultiServer.py | 174 ++++++++++++++++++++++++++++++--------- NetUtils.py | 41 +++++++-- Utils.py | 3 +- data/client.kv | 8 +- docs/network protocol.md | 24 ++++++ kvui.py | 96 ++++++++++++++++----- 8 files changed, 288 insertions(+), 72 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 47100a7383ab..fc6ae6d9a5fa 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -23,7 +23,7 @@ from MultiServer import CommandProcessor from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, - RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType) + RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType) from Utils import Version, stream_input, async_start from worlds import network_data_package, AutoWorldRegister import os @@ -412,6 +412,7 @@ async def disconnect(self, allow_autoreconnect: bool = False): await self.server.socket.close() if self.server_task is not None: await self.server_task + self.ui.update_hints() async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: """ `msgs` JSON serializable """ @@ -551,7 +552,14 @@ async def shutdown(self): await self.ui_task if self.input_task: self.input_task.cancel() - + + # Hints + def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None: + msg = {"cmd": "UpdateHint", "location": location, "player": finding_player} + if status is not None: + msg["status"] = status + async_start(self.send_msgs([msg]), name="update_hint") + # DataPackage async def prepare_data_package(self, relevant_games: typing.Set[str], remote_date_package_versions: typing.Dict[str, int], diff --git a/Main.py b/Main.py index 4008ca5e9017..6b94b84c278b 100644 --- a/Main.py +++ b/Main.py @@ -276,7 +276,7 @@ def write_multidata(): def precollect_hint(location): entrance = er_hint_data.get(location.player, {}).get(location.address, "") hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False, entrance, location.item.flags) + location.item.code, False, entrance, location.item.flags, False) precollected_hints[location.player].add(hint) if location.item.player not in multiworld.groups: precollected_hints[location.item.player].add(hint) diff --git a/MultiServer.py b/MultiServer.py index 847a0b281c40..0db8722b5cb6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -41,7 +41,8 @@ import Utils from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ - SlotType, LocationStore + SlotType, LocationStore, Hint, HintStatus +from BaseClasses import ItemClassification min_client_version = Version(0, 1, 6) colorama.init() @@ -228,7 +229,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.hint_cost = hint_cost self.location_check_points = location_check_points self.hints_used = collections.defaultdict(int) - self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set) + self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set) self.release_mode: str = release_mode self.remaining_mode: str = remaining_mode self.collect_mode: str = collect_mode @@ -656,13 +657,29 @@ def get_hint_cost(self, slot): return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) return 0 - def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): + def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None, + changed: typing.Optional[typing.Set[team_slot]] = None) -> None: + """Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot + will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot) + pair that has at least one hint modified will be added to the set. + """ for hint_team, hint_slot in self.hints: - if (team is None or team == hint_team) and (slot is None or slot == hint_slot): - self.hints[hint_team, hint_slot] = { - hint.re_check(self, hint_team) for hint in - self.hints[hint_team, hint_slot] - } + if team != hint_team and team is not None: + continue # Check specified team only, all if team is None + if slot != hint_slot and slot is not None: + continue # Check specified slot only, all if slot is None + new_hints: typing.Set[Hint] = set() + for hint in self.hints[hint_team, hint_slot]: + new_hint = hint.re_check(self, hint_team) + new_hints.add(new_hint) + if hint == new_hint: + continue + for player in self.slot_set(hint.receiving_player) | {hint.finding_player}: + if changed is not None: + changed.add((hint_team,player)) + if slot is not None and slot != player: + self.replace_hint(hint_team, player, hint, new_hint) + self.hints[hint_team, hint_slot] = new_hints def get_rechecked_hints(self, team: int, slot: int): self.recheck_hints(team, slot) @@ -711,7 +728,7 @@ def get_aliased_name(self, team: int, slot: int): else: return self.player_names[team, slot] - def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False, + def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False, recipients: typing.Sequence[int] = None): """Send and remember hints.""" if only_new: @@ -749,6 +766,17 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b for client in clients: async_start(self.send_msgs(client, client_hints)) + def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]: + for hint in self.hints[team, finding_player]: + if hint.location == seeked_location: + return hint + return None + + def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None: + if old_hint in self.hints[team, slot]: + self.hints[team, slot].remove(old_hint) + self.hints[team, slot].add(new_hint) + # "events" def on_goal_achieved(self, client: Client): @@ -1050,14 +1078,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi "hint_points": get_slot_points(ctx, team, slot), "checked_locations": new_locations, # send back new checks only }]) - old_hints = ctx.hints[team, slot].copy() - ctx.recheck_hints(team, slot) - if old_hints != ctx.hints[team, slot]: - ctx.on_changed_hints(team, slot) + updated_slots: typing.Set[tuple[int, int]] = set() + ctx.recheck_hints(team, slot, updated_slots) + for hint_team, hint_slot in updated_slots: + ctx.on_changed_hints(hint_team, hint_slot) ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \ + -> typing.List[Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): @@ -1067,31 +1096,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] for finding_player, location_id, item_id, receiving_player, item_flags \ in ctx.locations.find_item(slots, seeked_item_id): - found = location_id in ctx.location_checks[team, finding_player] - entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") - hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance, - item_flags)) + prev_hint = ctx.get_hint(team, slot, location_id) + if prev_hint: + hints.append(prev_hint) + else: + found = location_id in ctx.location_checks[team, finding_player] + entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance, + item_flags, new_status)) return hints -def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: +def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \ + -> typing.List[Hint]: seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] - return collect_hint_location_id(ctx, team, slot, seeked_location) + return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status) -def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]: +def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \ + -> typing.List[Hint]: + prev_hint = ctx.get_hint(team, slot, seeked_location) + if prev_hint: + return [prev_hint] result = ctx.locations[slot].get(seeked_location, (None, None, None)) if any(result): item_id, receiving_player, item_flags = result found = seeked_location in ctx.location_checks[team, slot] entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") - return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)] + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags, + new_status)] return [] -def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +def format_hint(ctx: Context, team: int, hint: Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ @@ -1099,7 +1155,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: if hint.entrance: text += f" at {hint.entrance}" - return text + (". (found)" if hint.found else ".") + + return text + ". " + status_names.get(hint.status, "(unknown)") def json_format_send_event(net_item: NetworkItem, receiving_player: int): @@ -1503,7 +1560,7 @@ def _cmd_getitem(self, item_name: str) -> bool: def get_hints(self, input_text: str, for_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) cost = self.ctx.get_hint_cost(self.client.slot) - + auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} @@ -1529,9 +1586,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] elif not for_location: - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: - hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: game = self.ctx.games[self.client.slot] @@ -1551,16 +1608,16 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: hints = [] for item_name in self.ctx.item_name_groups[game][hint_name]: if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status)) elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) elif hint_name in self.ctx.location_name_groups[game]: # location group name hints = [] for loc_name in self.ctx.location_name_groups[game][hint_name]: if loc_name in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) + hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status)) else: # location name - hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) else: self.output(response) @@ -1832,13 +1889,51 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): target_item, target_player, flags = ctx.locations[client.slot][location] if create_as_hint: - hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location)) + hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location, + HintStatus.HINT_UNSPECIFIED)) locs.append(NetworkItem(target_item, location, target_player, flags)) ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2) if locs and create_as_hint: ctx.save() await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) - + + elif cmd == 'UpdateHint': + location = args["location"] + player = args["player"] + status = args["status"] + if not isinstance(player, int) or not isinstance(location, int) \ + or (status is not None and not isinstance(status, int)): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint', + "original_cmd": cmd}]) + return + hint = ctx.get_hint(client.team, player, location) + if not hint: + return # Ignored safely + if hint.receiving_player != client.slot: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission', + "original_cmd": cmd}]) + return + new_hint = hint + if status is None: + return + try: + status = HintStatus(status) + except ValueError: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Invalid Status', "original_cmd": cmd}]) + return + new_hint = new_hint.re_prioritize(ctx, status) + if hint == new_hint: + return + ctx.replace_hint(client.team, hint.finding_player, hint, new_hint) + ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint) + ctx.save() + ctx.on_changed_hints(client.team, hint.finding_player) + ctx.on_changed_hints(client.team, hint.receiving_player) + elif cmd == 'StatusUpdate': update_client_status(ctx, client, args["status"]) @@ -2143,9 +2238,9 @@ def _cmd_hint(self, player_name: str, *item_name: str) -> bool: hints = [] for item_name_from_group in self.ctx.item_name_groups[game][item]: if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY)) else: # item name or id - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY) if hints: self.ctx.notify_hints(team, hints) @@ -2179,14 +2274,17 @@ def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: if usable: if isinstance(location, int): - hints = collect_hint_location_id(self.ctx, team, slot, location) + hints = collect_hint_location_id(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]: hints = [] for loc_name_from_group in self.ctx.location_name_groups[game][location]: if loc_name_from_group in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group)) + hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group, + HintStatus.HINT_UNSPECIFIED)) else: - hints = collect_hint_location_name(self.ctx, team, slot, location) + hints = collect_hint_location_name(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) if hints: self.ctx.notify_hints(team, hints) else: diff --git a/NetUtils.py b/NetUtils.py index 4776b228db17..ec6ff3eb1d81 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -29,6 +29,14 @@ class ClientStatus(ByValue, enum.IntEnum): CLIENT_GOAL = 30 +class HintStatus(enum.IntEnum): + HINT_FOUND = 0 + HINT_UNSPECIFIED = 1 + HINT_NO_PRIORITY = 10 + HINT_AVOID = 20 + HINT_PRIORITY = 30 + + class SlotType(ByValue, enum.IntFlag): spectator = 0b00 player = 0b01 @@ -297,6 +305,20 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs}) +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +status_colors: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "green", + HintStatus.HINT_UNSPECIFIED: "white", + HintStatus.HINT_NO_PRIORITY: "slateblue", + HintStatus.HINT_AVOID: "salmon", + HintStatus.HINT_PRIORITY: "plum", +} class Hint(typing.NamedTuple): receiving_player: int finding_player: int @@ -305,14 +327,21 @@ class Hint(typing.NamedTuple): found: bool entrance: str = "" item_flags: int = 0 + status: HintStatus = HintStatus.HINT_UNSPECIFIED def re_check(self, ctx, team) -> Hint: - if self.found: + if self.found and self.status == HintStatus.HINT_FOUND: return self found = self.location in ctx.location_checks[team, self.finding_player] if found: - return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance, - self.item_flags) + return self._replace(found=found, status=HintStatus.HINT_FOUND) + return self + + def re_prioritize(self, ctx, status: HintStatus) -> Hint: + if self.found and status != HintStatus.HINT_FOUND: + status = HintStatus.HINT_FOUND + if status != self.status: + return self._replace(status=status) return self def __hash__(self): @@ -334,10 +363,8 @@ def as_network_message(self) -> dict: else: add_json_text(parts, "'s World") add_json_text(parts, ". ") - if self.found: - add_json_text(parts, "(found)", type="color", color="green") - else: - add_json_text(parts, "(not found)", type="color", color="red") + add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color", + color=status_colors.get(self.status, "red")) return {"cmd": "PrintJSON", "data": parts, "type": "Hint", "receiving": self.receiving_player, diff --git a/Utils.py b/Utils.py index 535933d815b1..cd0a89713199 100644 --- a/Utils.py +++ b/Utils.py @@ -421,7 +421,8 @@ def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata - if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: + if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", + "SlotType", "NetworkSlot", "HintStatus"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate if module == "worlds.generic" and name == "PlandoItem": diff --git a/data/client.kv b/data/client.kv index dc8a5c9c9d72..3455f2a23657 100644 --- a/data/client.kv +++ b/data/client.kv @@ -59,7 +59,7 @@ finding_text: "Finding Player" location_text: "Location" entrance_text: "Entrance" - found_text: "Found?" + status_text: "Status" TooltipLabel: id: receiving sort_key: 'receiving' @@ -96,9 +96,9 @@ valign: 'center' pos_hint: {"center_y": 0.5} TooltipLabel: - id: found - sort_key: 'found' - text: root.found_text + id: status + sort_key: 'status' + text: root.status_text halign: 'center' valign: 'center' pos_hint: {"center_y": 0.5} diff --git a/docs/network protocol.md b/docs/network protocol.md index 4a96a43f818f..1c5b2e002289 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -272,6 +272,7 @@ These packets are sent purely from client to server. They are not accepted by cl * [Sync](#Sync) * [LocationChecks](#LocationChecks) * [LocationScouts](#LocationScouts) +* [UpdateHint](#UpdateHint) * [StatusUpdate](#StatusUpdate) * [Say](#Say) * [GetDataPackage](#GetDataPackage) @@ -342,6 +343,29 @@ This is useful in cases where an item appears in the game world, such as 'ledge | locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. | | create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint.
If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. | +### UpdateHint +Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails. + +### Arguments +| Name | Type | Notes | +| ---- | ---- | ----- | +| player | int | The ID of the player whose location is being hinted for. | +| location | int | The ID of the location to update the hint for. If no hint exists for this location, the packet is ignored. | +| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. | + +#### HintStatus +An enumeration containing the possible hint states. + +```python +import enum +class HintStatus(enum.IntEnum): + HINT_FOUND = 0 + HINT_UNSPECIFIED = 1 + HINT_NO_PRIORITY = 10 + HINT_AVOID = 20 + HINT_PRIORITY = 30 +``` + ### StatusUpdate Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past) diff --git a/kvui.py b/kvui.py index 2723654214c1..dfe935930049 100644 --- a/kvui.py +++ b/kvui.py @@ -52,6 +52,7 @@ from kivy.uix.floatlayout import FloatLayout from kivy.uix.label import Label from kivy.uix.progressbar import ProgressBar +from kivy.uix.dropdown import DropDown from kivy.utils import escape_markup from kivy.lang import Builder from kivy.uix.recycleview.views import RecycleDataViewBehavior @@ -63,7 +64,7 @@ fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) -from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType +from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus from Utils import async_start, get_input_text_from_response if typing.TYPE_CHECKING: @@ -300,11 +301,11 @@ def apply_selection(self, rv, index, is_selected): """ Respond to the selection of items in the view. """ self.selected = is_selected - class HintLabel(RecycleDataViewBehavior, BoxLayout): selected = BooleanProperty(False) striped = BooleanProperty(False) index = None + dropdown: DropDown def __init__(self): super(HintLabel, self).__init__() @@ -313,10 +314,32 @@ def __init__(self): self.finding_text = "" self.location_text = "" self.entrance_text = "" - self.found_text = "" + self.status_text = "" + self.hint = {} for child in self.children: child.bind(texture_size=self.set_height) + + ctx = App.get_running_app().ctx + self.dropdown = DropDown() + + def set_value(button): + self.dropdown.select(button.status) + + def select(instance, data): + ctx.update_hint(self.hint["location"], + self.hint["finding_player"], + data) + + for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID): + name = status_names[status] + status_button = Button(text=name, size_hint_y=None, height=dp(50)) + status_button.status = status + status_button.bind(on_release=set_value) + self.dropdown.add_widget(status_button) + + self.dropdown.bind(on_select=select) + def set_height(self, instance, value): self.height = max([child.texture_size[1] for child in self.children]) @@ -328,7 +351,8 @@ def refresh_view_attrs(self, rv, index, data): self.finding_text = data["finding"]["text"] self.location_text = data["location"]["text"] self.entrance_text = data["entrance"]["text"] - self.found_text = data["found"]["text"] + self.status_text = data["status"]["text"] + self.hint = data["status"]["hint"] self.height = self.minimum_height return super(HintLabel, self).refresh_view_attrs(rv, index, data) @@ -338,13 +362,21 @@ def on_touch_down(self, touch): return True if self.index: # skip header if self.collide_point(*touch.pos): - if self.selected: + status_label = self.ids["status"] + if status_label.collide_point(*touch.pos): + if self.hint["status"] == HintStatus.HINT_FOUND: + return + ctx = App.get_running_app().ctx + if ctx.slot == self.hint["receiving_player"]: # If this player owns this hint + # open a dropdown + self.dropdown.open(self.ids["status"]) + elif self.selected: self.parent.clear_selection() else: text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ", self.finding_text, "\'s World", (" at " + self.entrance_text) if self.entrance_text != "Vanilla" - else "", ". (", self.found_text.lower(), ")")) + else "", ". (", self.status_text.lower(), ")")) temp = MarkupLabel(text).markup text = "".join( part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) @@ -358,18 +390,16 @@ def on_touch_down(self, touch): for child in self.children: if child.collide_point(*touch.pos): key = child.sort_key - parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower() + if key == "status": + parent.hint_sorter = lambda element: element["status"]["hint"]["status"] + else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower() if key == parent.sort_key: # second click reverses order parent.reversed = not parent.reversed else: parent.sort_key = key parent.reversed = False - break - else: - logging.warning("Did not find clicked header for sorting.") - - App.get_running_app().update_hints() + App.get_running_app().update_hints() def apply_selection(self, rv, index, is_selected): """ Respond to the selection of items in the view. """ @@ -663,7 +693,7 @@ def set_new_energy_link_value(self): self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J" def update_hints(self): - hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"] + hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", []) self.log_panels["Hints"].refresh_hints(hints) # default F1 keybind, opens a settings menu, that seems to break the layout engine once closed @@ -719,6 +749,22 @@ def fix_heights(self): element.height = element.texture_size[1] +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "Found", + HintStatus.HINT_UNSPECIFIED: "Unspecified", + HintStatus.HINT_NO_PRIORITY: "No Priority", + HintStatus.HINT_AVOID: "Avoid", + HintStatus.HINT_PRIORITY: "Priority", +} +status_colors: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "green", + HintStatus.HINT_UNSPECIFIED: "white", + HintStatus.HINT_NO_PRIORITY: "cyan", + HintStatus.HINT_AVOID: "salmon", + HintStatus.HINT_PRIORITY: "plum", +} + + class HintLog(RecycleView): header = { "receiving": {"text": "[u]Receiving Player[/u]"}, @@ -726,12 +772,13 @@ class HintLog(RecycleView): "finding": {"text": "[u]Finding Player[/u]"}, "location": {"text": "[u]Location[/u]"}, "entrance": {"text": "[u]Entrance[/u]"}, - "found": {"text": "[u]Status[/u]"}, + "status": {"text": "[u]Status[/u]", + "hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}}, "striped": True, } sort_key: str = "" - reversed: bool = False + reversed: bool = True def __init__(self, parser): super(HintLog, self).__init__() @@ -739,8 +786,18 @@ def __init__(self, parser): self.parser = parser def refresh_hints(self, hints): + if not hints: # Fix the scrolling looking visually wrong in some edge cases + self.scroll_y = 1.0 data = [] + ctx = App.get_running_app().ctx for hint in hints: + if not hint.get("status"): # Allows connecting to old servers + hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED + hint_status_node = self.parser.handle_node({"type": "color", + "color": status_colors.get(hint["status"], "red"), + "text": status_names.get(hint["status"], "Unknown")}) + if hint["status"] != HintStatus.HINT_FOUND and hint["receiving_player"] == ctx.slot: + hint_status_node = f"[u]{hint_status_node}[/u]" data.append({ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, "item": {"text": self.parser.handle_node({ @@ -758,9 +815,10 @@ def refresh_hints(self, hints): "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", "color": "blue", "text": hint["entrance"] if hint["entrance"] else "Vanilla"})}, - "found": { - "text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red", - "text": "Found" if hint["found"] else "Not Found"})}, + "status": { + "text": hint_status_node, + "hint": hint, + }, }) data.sort(key=self.hint_sorter, reverse=self.reversed) @@ -771,7 +829,7 @@ def refresh_hints(self, hints): @staticmethod def hint_sorter(element: dict) -> str: - return "" + return element["status"]["hint"]["status"] # By status by default def fix_heights(self): """Workaround fix for divergent texture and layout heights""" From 8923b06a49692322168537a7ddbc56fef15ab5d2 Mon Sep 17 00:00:00 2001 From: BootsinSoots <102177943+BootsinSoots@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:16:12 -0500 Subject: [PATCH 021/112] Webhost: Make YGO 06 setup title match page #4262 Make Guide title match the rest of the set up guides on the webhost --- worlds/yugioh06/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index a39b52cd09d5..90bbed1a2174 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -50,7 +50,7 @@ class Yugioh06Web(WebWorld): theme = "stone" setup = Tutorial( - "Multiworld Setup Tutorial", + "Multiworld Setup Guide", "A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 " "for Archipelago on your computer.", "English", From ce210cd4ee27bd9b171d3fb0d9ddb776f7d985f4 Mon Sep 17 00:00:00 2001 From: Solidus Snake <63137482+dontjoome@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:16:50 -0500 Subject: [PATCH 022/112] SMZ3: Add Start Inventory From Pool (#4252) * Add Start Inventory From Pool Just as the title implies * Update Options.py Fix dataclass since I had just pulled changes from prior options.py without seeing if anythin had changed * Update Options.py One more time with feeling * Update worlds/smz3/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/smz3/Options.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index 7df01f8710e1..02521d695a7a 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility + +from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility, StartInventoryPool from dataclasses import dataclass class SMLogic(Choice): @@ -129,6 +130,7 @@ class EnergyBeep(DefaultOnToggle): @dataclass class SMZ3Options(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool accessibility: ItemsAccessibility sm_logic: SMLogic sword_location: SwordLocation From 30b414429f5b118c9f0b77537d6aac464510615f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 29 Nov 2024 05:02:26 +0100 Subject: [PATCH 023/112] LTTP: sort of use new options system (#3764) * LttP: switch to dataclass options definition * LttP: write old options onto multiworld LttP: use World.random --- worlds/alttp/Options.py | 166 +++++++++++++++++++-------------------- worlds/alttp/Rom.py | 9 ++- worlds/alttp/__init__.py | 35 +++++---- 3 files changed, 109 insertions(+), 101 deletions(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index bd87cbf2c3ea..097458611734 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,7 +1,7 @@ -import typing +from dataclasses import dataclass from BaseClasses import MultiWorld -from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \ +from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, PerGameCommonOptions, \ PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle from .EntranceShuffle import default_connections, default_dungeon_connections, \ inverted_default_connections, inverted_default_dungeon_connections @@ -742,86 +742,86 @@ class ALttPPlandoTexts(PlandoTexts): valid_keys = TextTable.valid_keys -alttp_options: typing.Dict[str, type(Option)] = { - "accessibility": ItemsAccessibility, - "plando_connections": ALttPPlandoConnections, - "plando_texts": ALttPPlandoTexts, - "start_inventory_from_pool": StartInventoryPool, - "goal": Goal, - "mode": Mode, - "glitches_required": GlitchesRequired, - "dark_room_logic": DarkRoomLogic, - "open_pyramid": OpenPyramid, - "crystals_needed_for_gt": CrystalsTower, - "crystals_needed_for_ganon": CrystalsGanon, - "triforce_pieces_mode": TriforcePiecesMode, - "triforce_pieces_percentage": TriforcePiecesPercentage, - "triforce_pieces_required": TriforcePiecesRequired, - "triforce_pieces_available": TriforcePiecesAvailable, - "triforce_pieces_extra": TriforcePiecesExtra, - "entrance_shuffle": EntranceShuffle, - "entrance_shuffle_seed": EntranceShuffleSeed, - "big_key_shuffle": big_key_shuffle, - "small_key_shuffle": small_key_shuffle, - "key_drop_shuffle": key_drop_shuffle, - "compass_shuffle": compass_shuffle, - "map_shuffle": map_shuffle, - "restrict_dungeon_item_on_boss": RestrictBossItem, - "item_pool": ItemPool, - "item_functionality": ItemFunctionality, - "enemy_health": EnemyHealth, - "enemy_damage": EnemyDamage, - "progressive": Progressive, - "swordless": Swordless, - "dungeon_counters": DungeonCounters, - "retro_bow": RetroBow, - "retro_caves": RetroCaves, - "hints": Hints, - "scams": Scams, - "boss_shuffle": LTTPBosses, - "pot_shuffle": PotShuffle, - "enemy_shuffle": EnemyShuffle, - "killable_thieves": KillableThieves, - "bush_shuffle": BushShuffle, - "shop_item_slots": ShopItemSlots, - "randomize_shop_inventories": RandomizeShopInventories, - "shuffle_shop_inventories": ShuffleShopInventories, - "include_witch_hut": IncludeWitchHut, - "randomize_shop_prices": RandomizeShopPrices, - "randomize_cost_types": RandomizeCostTypes, - "shop_price_modifier": ShopPriceModifier, - "shuffle_capacity_upgrades": ShuffleCapacityUpgrades, - "bombless_start": BomblessStart, - "shuffle_prizes": ShufflePrizes, - "tile_shuffle": TileShuffle, - "misery_mire_medallion": MiseryMireMedallion, - "turtle_rock_medallion": TurtleRockMedallion, - "glitch_boots": GlitchBoots, - "beemizer_total_chance": BeemizerTotalChance, - "beemizer_trap_chance": BeemizerTrapChance, - "timer": Timer, - "countdown_start_time": CountdownStartTime, - "red_clock_time": RedClockTime, - "blue_clock_time": BlueClockTime, - "green_clock_time": GreenClockTime, - "death_link": DeathLink, - "allow_collect": AllowCollect, - "ow_palettes": OWPalette, - "uw_palettes": UWPalette, - "hud_palettes": HUDPalette, - "sword_palettes": SwordPalette, - "shield_palettes": ShieldPalette, - # "link_palettes": LinkPalette, - "heartbeep": HeartBeep, - "heartcolor": HeartColor, - "quickswap": QuickSwap, - "menuspeed": MenuSpeed, - "music": Music, - "reduceflashing": ReduceFlashing, - "triforcehud": TriforceHud, +@dataclass +class ALTTPOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility + plando_connections: ALttPPlandoConnections + plando_texts: ALttPPlandoTexts + start_inventory_from_pool: StartInventoryPool + goal: Goal + mode: Mode + glitches_required: GlitchesRequired + dark_room_logic: DarkRoomLogic + open_pyramid: OpenPyramid + crystals_needed_for_gt: CrystalsTower + crystals_needed_for_ganon: CrystalsGanon + triforce_pieces_mode: TriforcePiecesMode + triforce_pieces_percentage: TriforcePiecesPercentage + triforce_pieces_required: TriforcePiecesRequired + triforce_pieces_available: TriforcePiecesAvailable + triforce_pieces_extra: TriforcePiecesExtra + entrance_shuffle: EntranceShuffle + entrance_shuffle_seed: EntranceShuffleSeed + big_key_shuffle: big_key_shuffle + small_key_shuffle: small_key_shuffle + key_drop_shuffle: key_drop_shuffle + compass_shuffle: compass_shuffle + map_shuffle: map_shuffle + restrict_dungeon_item_on_boss: RestrictBossItem + item_pool: ItemPool + item_functionality: ItemFunctionality + enemy_health: EnemyHealth + enemy_damage: EnemyDamage + progressive: Progressive + swordless: Swordless + dungeon_counters: DungeonCounters + retro_bow: RetroBow + retro_caves: RetroCaves + hints: Hints + scams: Scams + boss_shuffle: LTTPBosses + pot_shuffle: PotShuffle + enemy_shuffle: EnemyShuffle + killable_thieves: KillableThieves + bush_shuffle: BushShuffle + shop_item_slots: ShopItemSlots + randomize_shop_inventories: RandomizeShopInventories + shuffle_shop_inventories: ShuffleShopInventories + include_witch_hut: IncludeWitchHut + randomize_shop_prices: RandomizeShopPrices + randomize_cost_types: RandomizeCostTypes + shop_price_modifier: ShopPriceModifier + shuffle_capacity_upgrades: ShuffleCapacityUpgrades + bombless_start: BomblessStart + shuffle_prizes: ShufflePrizes + tile_shuffle: TileShuffle + misery_mire_medallion: MiseryMireMedallion + turtle_rock_medallion: TurtleRockMedallion + glitch_boots: GlitchBoots + beemizer_total_chance: BeemizerTotalChance + beemizer_trap_chance: BeemizerTrapChance + timer: Timer + countdown_start_time: CountdownStartTime + red_clock_time: RedClockTime + blue_clock_time: BlueClockTime + green_clock_time: GreenClockTime + death_link: DeathLink + allow_collect: AllowCollect + ow_palettes: OWPalette + uw_palettes: UWPalette + hud_palettes: HUDPalette + sword_palettes: SwordPalette + shield_palettes: ShieldPalette + # link_palettes: LinkPalette + heartbeep: HeartBeep + heartcolor: HeartColor + quickswap: QuickSwap + menuspeed: MenuSpeed + music: Music + reduceflashing: ReduceFlashing + triforcehud: TriforceHud # removed: - "goals": Removed, - "smallkey_shuffle": Removed, - "bigkey_shuffle": Removed, -} + goals: Removed + smallkey_shuffle: Removed + bigkey_shuffle: Removed diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 224de6aaf7f3..73a77b03f532 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -782,8 +782,8 @@ def get_nonnative_item_sprite(code: int) -> int: def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): - local_random = world.per_slot_randoms[player] local_world = world.worlds[player] + local_random = local_world.random # patch items @@ -1867,7 +1867,7 @@ def apply_oof_sfx(rom, oof: str): def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options, world=None, player=1, allow_random_on_event=False, reduceflashing=False, triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False): - local_random = random if not world else world.per_slot_randoms[player] + local_random = random if not world else world.worlds[player].random disable_music: bool = not music # enable instant item menu if menuspeed == 'instant': @@ -2197,8 +2197,9 @@ def write_string_to_rom(rom, target, string): def write_strings(rom, world, player): from . import ALTTPWorld - local_random = world.per_slot_randoms[player] + w: ALTTPWorld = world.worlds[player] + local_random = w.random tt = TextTable() tt.removeUnwantedText() @@ -2425,7 +2426,7 @@ def hint_text(dest, ped_hint=False): if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or ( world.swordless[player] or world.glitches_required[player] == 'no_glitches')): prog_bow_locs = world.find_item_locations('Progressive Bow', player, True) - world.per_slot_randoms[player].shuffle(prog_bow_locs) + local_random.shuffle(prog_bow_locs) found_bow = False found_bow_alt = False while prog_bow_locs and not (found_bow and found_bow_alt): diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index f897d3762929..b5489906889f 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -1,28 +1,27 @@ import logging import os import random -import settings import threading import typing -import Utils +import settings from BaseClasses import Item, CollectionState, Tutorial, MultiWorld +from worlds.AutoWorld import World, WebWorld, LogicMixin +from .Client import ALTTPSNIClient from .Dungeons import create_dungeons, Dungeon from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem -from .Options import alttp_options, small_key_shuffle +from .Options import ALTTPOptions, small_key_shuffle from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ is_main_entrance, key_drop_data -from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name -from .SubClasses import ALttPItem, LTTPRegionType -from worlds.AutoWorld import World, WebWorld, LogicMixin from .StateHelpers import can_buy_unlimited +from .SubClasses import ALttPItem, LTTPRegionType lttp_logger = logging.getLogger("A Link to the Past") @@ -132,7 +131,8 @@ class ALTTPWorld(World): Ganon! """ game = "A Link to the Past" - option_definitions = alttp_options + options_dataclass = ALTTPOptions + options: ALTTPOptions settings_key = "lttp_options" settings: typing.ClassVar[ALTTPSettings] topology_present = True @@ -286,13 +286,22 @@ def stage_assert_generate(cls, multiworld: MultiWorld): if not os.path.exists(rom_file): raise FileNotFoundError(rom_file) if multiworld.is_race: - import xxtea + import xxtea # noqa for player in multiworld.get_game_players(cls.game): if multiworld.worlds[player].use_enemizer: check_enemizer(multiworld.worlds[player].enemizer_path) break def generate_early(self): + # write old options + import dataclasses + is_first = self.player == min(self.multiworld.get_game_players(self.game)) + + for field in dataclasses.fields(self.options_dataclass): + if is_first: + setattr(self.multiworld, field.name, {}) + getattr(self.multiworld, field.name)[self.player] = getattr(self.options, field.name) + # end of old options re-establisher player = self.player multiworld = self.multiworld @@ -536,12 +545,10 @@ def stage_generate_output(cls, multiworld, output_directory): @property def use_enemizer(self) -> bool: - world = self.multiworld - player = self.player - return bool(world.boss_shuffle[player] or world.enemy_shuffle[player] - or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or world.pot_shuffle[player] or world.bush_shuffle[player] - or world.killable_thieves[player]) + return bool(self.options.boss_shuffle or self.options.enemy_shuffle + or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default' + or self.options.pot_shuffle or self.options.bush_shuffle + or self.options.killable_thieves) def generate_output(self, output_directory: str): multiworld = self.multiworld From 1371c63a8debded46b764ac1953134b1f0491bfa Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 07:14:23 +0100 Subject: [PATCH 024/112] Core: Actually take item from pool when plandoing from_pool (#2420) * Actually take item from pool when plandoing from_pool * Remove the awkward index thing * oops left a comment in * there wasn't a line break here before * Only remove if actually found, check against player number * oops * Go back to index based system so we can just remove at the end * Comment * Fix error on None * Update Fill.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- Fill.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index 706cca657457..912b4d05bed9 100644 --- a/Fill.py +++ b/Fill.py @@ -978,15 +978,32 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: multiworld.random.shuffle(items) count = 0 err: typing.List[str] = [] - successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] + successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = [] + claimed_indices: typing.Set[typing.Optional[int]] = set() for item_name in items: - item = multiworld.worlds[player].create_item(item_name) + index_to_delete: typing.Optional[int] = None + if from_pool: + try: + # If from_pool, try to find an existing item with this name & player in the itempool and use it + index_to_delete, item = next( + (i, item) for i, item in enumerate(multiworld.itempool) + if item.player == player and item.name == item_name and i not in claimed_indices + ) + except StopIteration: + warn( + f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.", + placement['force']) + item = multiworld.worlds[player].create_item(item_name) + else: + item = multiworld.worlds[player].create_item(item_name) + for location in reversed(candidates): if (location.address is None) == (item.code is None): # either both None or both not None if not location.item: if location.item_rule(item): if location.can_fill(multiworld.state, item, False): - successful_pairs.append((item, location)) + successful_pairs.append((index_to_delete, item, location)) + claimed_indices.add(index_to_delete) candidates.remove(location) count = count + 1 break @@ -998,6 +1015,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: err.append(f"Cannot place {item_name} into already filled location {location}.") else: err.append(f"Mismatch between {item_name} and {location}, only one is an event.") + if count == maxcount: break if count < placement['count']['min']: @@ -1005,17 +1023,16 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: failed( f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}", placement['force']) - for (item, location) in successful_pairs: + + # Sort indices in reverse so we can remove them one by one + successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True) + + for (index, item, location) in successful_pairs: multiworld.push_item(location, item, collect=False) location.locked = True logging.debug(f"Plando placed {item} at {location}") - if from_pool: - try: - multiworld.itempool.remove(item) - except ValueError: - warn( - f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", - placement['force']) + if index is not None: # If this item is from_pool and was found in the pool, remove it. + multiworld.itempool.pop(index) except Exception as e: raise Exception( From 91185f4f7c84b93ef5c7e9ee95665022e89d1a90 Mon Sep 17 00:00:00 2001 From: ken Date: Thu, 28 Nov 2024 22:16:54 -0800 Subject: [PATCH 025/112] Core: Add timestamps to logging for seed generation (#3028) * Add timestamps to logging for improved debugging * Add datetime to general logging; particularly useful for large seeds. * Move console timestamps from Main to Utils.init_logging (better location) * Update Main.py remove spurious blank line Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --------- Co-authored-by: Zach Parks Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- Utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Utils.py b/Utils.py index cd0a89713199..4e8d7fc427e0 100644 --- a/Utils.py +++ b/Utils.py @@ -515,10 +515,13 @@ def filter(self, record: logging.LogRecord) -> bool: return self.condition(record) file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg)) root_logger.addHandler(file_handler) if sys.stdout: + formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') stream_handler = logging.StreamHandler(sys.stdout) stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False))) + stream_handler.setFormatter(formatter) root_logger.addHandler(stream_handler) # Relay unhandled exceptions to logger. From 6f2464d4ad5023fec18d466bd7a77b69c003a7e8 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Fri, 29 Nov 2024 00:24:24 -0800 Subject: [PATCH 026/112] Pokemon Emerald: Rework tags/dynamically create item and location groups (#3263) * Pokemon Emerald: Rework location tags to categories * Pokemon Emerald: Rework item tags, automatically create item/location groups * Pokemon Emerald: Move item and location groups to data.py, add some regional location groups * Map Regions * Pokemon Emerald: Fix up location groups * Pokemon Emerald: Move groups to their own file * Pokemon Emerald: Add meta groups for location groups * Pokemon Emerald: Fix has_group using updated item group name * Pokemon Emerald: Add sanity check for maps in location groups * Pokemon Emerald: Remove missed use of location.tags * Pokemon Emerald: Reclassify white and black flutes * Pokemon Emerald: Update changelog * Pokemon Emerald: Adjust changelog --------- Co-authored-by: Tsukino <16899482+Tsukino-uwu@users.noreply.github.com> --- worlds/pokemon_emerald/CHANGELOG.md | 1 + worlds/pokemon_emerald/__init__.py | 83 +- worlds/pokemon_emerald/data.py | 19 + worlds/pokemon_emerald/data/items.json | 102 +- worlds/pokemon_emerald/data/locations.json | 4014 +++++++++++++------- worlds/pokemon_emerald/groups.py | 721 ++++ worlds/pokemon_emerald/items.py | 26 +- worlds/pokemon_emerald/locations.py | 64 +- worlds/pokemon_emerald/rules.py | 14 +- worlds/pokemon_emerald/sanity_check.py | 19 +- 10 files changed, 3551 insertions(+), 1512 deletions(-) create mode 100644 worlds/pokemon_emerald/groups.py diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 0dd874b25029..72005d6f9d3f 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- Added many new item and location groups. - Added a Swedish translation of the setup guide. - The client communicates map transitions to any trackers connected to the slot. - Added the player's Normalize Encounter Rates option to slot data for trackers. diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index a87f93ece56b..c99a0c11cdfb 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -15,11 +15,11 @@ from worlds.AutoWorld import WebWorld, World from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient -from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, data as emerald_data -from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, - offset_item_value) -from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map, - create_locations_with_tags, set_free_fly, set_legendary_cave_entrances) +from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data +from .groups import ITEM_GROUPS, LOCATION_GROUPS +from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value +from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category, + set_free_fly, set_legendary_cave_entrances) from .opponents import randomize_opponent_parties from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions, RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement) @@ -133,9 +133,10 @@ def __init__(self, multiworld, player): @classmethod def stage_assert_generate(cls, multiworld: MultiWorld) -> None: - from .sanity_check import validate_regions + from .sanity_check import validate_regions, validate_group_maps assert validate_regions() + assert validate_group_maps() def get_filler_item_name(self) -> str: return "Great Ball" @@ -237,24 +238,32 @@ def generate_early(self) -> None: def create_regions(self) -> None: from .regions import create_regions - regions = create_regions(self) - - tags = {"Badge", "HM", "KeyItem", "Rod", "Bike", "EventTicket"} # Tags with progression items always included + all_regions = create_regions(self) + + # Categories with progression items always included + categories = { + LocationCategory.BADGE, + LocationCategory.HM, + LocationCategory.KEY, + LocationCategory.ROD, + LocationCategory.BIKE, + LocationCategory.TICKET + } if self.options.overworld_items: - tags.add("OverworldItem") + categories.add(LocationCategory.OVERWORLD_ITEM) if self.options.hidden_items: - tags.add("HiddenItem") + categories.add(LocationCategory.HIDDEN_ITEM) if self.options.npc_gifts: - tags.add("NpcGift") + categories.add(LocationCategory.GIFT) if self.options.berry_trees: - tags.add("BerryTree") + categories.add(LocationCategory.BERRY_TREE) if self.options.dexsanity: - tags.add("Pokedex") + categories.add(LocationCategory.POKEDEX) if self.options.trainersanity: - tags.add("Trainer") - create_locations_with_tags(self, regions, tags) + categories.add(LocationCategory.TRAINER) + create_locations_by_category(self, all_regions, categories) - self.multiworld.regions.extend(regions.values()) + self.multiworld.regions.extend(all_regions.values()) # Exclude locations which are always locked behind the player's goal def exclude_locations(location_names: List[str]): @@ -325,21 +334,21 @@ def create_items(self) -> None: # Filter progression items which shouldn't be shuffled into the itempool. # Their locations will still exist, but event items will be placed and # locked at their vanilla locations instead. - filter_tags = set() + filter_categories = set() if not self.options.key_items: - filter_tags.add("KeyItem") + filter_categories.add(LocationCategory.KEY) if not self.options.rods: - filter_tags.add("Rod") + filter_categories.add(LocationCategory.ROD) if not self.options.bikes: - filter_tags.add("Bike") + filter_categories.add(LocationCategory.BIKE) if not self.options.event_tickets: - filter_tags.add("EventTicket") + filter_categories.add(LocationCategory.TICKET) if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}: - filter_tags.add("Badge") + filter_categories.add(LocationCategory.BADGE) if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}: - filter_tags.add("HM") + filter_categories.add(LocationCategory.HM) # If Badges and HMs are set to the `shuffle` option, don't add them to # the normal item pool, but do create their items and save them and @@ -347,17 +356,17 @@ def create_items(self) -> None: if self.options.badges == RandomizeBadges.option_shuffle: self.badge_shuffle_info = [ (location, self.create_item_by_code(location.default_item_code)) - for location in [l for l in item_locations if "Badge" in l.tags] + for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.BADGE] ] if self.options.hms == RandomizeHms.option_shuffle: self.hm_shuffle_info = [ (location, self.create_item_by_code(location.default_item_code)) - for location in [l for l in item_locations if "HM" in l.tags] + for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.HM] ] # Filter down locations to actual items that will be filled and create # the itempool. - item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0] + item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories] default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations] # Take the itempool as-is @@ -366,7 +375,8 @@ def create_items(self) -> None: # Recreate the itempool from random items elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced): - item_categories = ["Ball", "Heal", "Candy", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc", "Berry"] + item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone", + "Money", "TM", "Held", "Misc", "Berry"] # Count occurrences of types of vanilla items in pool item_category_counter = Counter() @@ -436,25 +446,26 @@ def generate_basic(self) -> None: # Key items which are considered in access rules but not randomized are converted to events and placed # in their vanilla locations so that the player can have them in their inventory for logic. - def convert_unrandomized_items_to_events(tag: str) -> None: + def convert_unrandomized_items_to_events(category: LocationCategory) -> None: for location in self.multiworld.get_locations(self.player): - if location.tags is not None and tag in location.tags: + assert isinstance(location, PokemonEmeraldLocation) + if location.key is not None and emerald_data.locations[location.key].category == category: location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code])) location.progress_type = LocationProgressType.DEFAULT location.address = None if self.options.badges == RandomizeBadges.option_vanilla: - convert_unrandomized_items_to_events("Badge") + convert_unrandomized_items_to_events(LocationCategory.BADGE) if self.options.hms == RandomizeHms.option_vanilla: - convert_unrandomized_items_to_events("HM") + convert_unrandomized_items_to_events(LocationCategory.HM) if not self.options.rods: - convert_unrandomized_items_to_events("Rod") + convert_unrandomized_items_to_events(LocationCategory.ROD) if not self.options.bikes: - convert_unrandomized_items_to_events("Bike") + convert_unrandomized_items_to_events(LocationCategory.BIKE) if not self.options.event_tickets: - convert_unrandomized_items_to_events("EventTicket") + convert_unrandomized_items_to_events(LocationCategory.TICKET) if not self.options.key_items: - convert_unrandomized_items_to_events("KeyItem") + convert_unrandomized_items_to_events(LocationCategory.KEY) def pre_fill(self) -> None: # Badges and HMs that are set to shuffle need to be placed at diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index 432d59387391..c7af5ef2284a 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -117,6 +117,21 @@ class ItemData(NamedTuple): tags: FrozenSet[str] +class LocationCategory(IntEnum): + BADGE = 0 + HM = 1 + KEY = 2 + ROD = 3 + BIKE = 4 + TICKET = 5 + OVERWORLD_ITEM = 6 + HIDDEN_ITEM = 7 + GIFT = 8 + BERRY_TREE = 9 + TRAINER = 10 + POKEDEX = 11 + + class LocationData(NamedTuple): name: str label: str @@ -124,6 +139,7 @@ class LocationData(NamedTuple): default_item: int address: Union[int, List[int]] flag: int + category: LocationCategory tags: FrozenSet[str] @@ -431,6 +447,7 @@ def _init() -> None: location_json["default_item"], [location_json["address"]] + [j["address"] for j in alternate_rival_jsons], location_json["flag"], + LocationCategory[location_attributes_json[location_name]["category"]], frozenset(location_attributes_json[location_name]["tags"]) ) else: @@ -441,6 +458,7 @@ def _init() -> None: location_json["default_item"], location_json["address"], location_json["flag"], + LocationCategory[location_attributes_json[location_name]["category"]], frozenset(location_attributes_json[location_name]["tags"]) ) new_region.locations.append(location_name) @@ -948,6 +966,7 @@ def _init() -> None: evo_stage_to_ball_map[evo_stage], data.locations[dex_location_name].address, data.locations[dex_location_name].flag, + data.locations[dex_location_name].category, data.locations[dex_location_name].tags ) diff --git a/worlds/pokemon_emerald/data/items.json b/worlds/pokemon_emerald/data/items.json index 139d75aad0ab..4c09d215cf3c 100644 --- a/worlds/pokemon_emerald/data/items.json +++ b/worlds/pokemon_emerald/data/items.json @@ -52,49 +52,49 @@ "ITEM_HM_CUT": { "label": "HM01 Cut", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM01", "Unique"], "modern_id": 420 }, "ITEM_HM_FLY": { "label": "HM02 Fly", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM02", "Unique"], "modern_id": 421 }, "ITEM_HM_SURF": { "label": "HM03 Surf", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM03", "Unique"], "modern_id": 422 }, "ITEM_HM_STRENGTH": { "label": "HM04 Strength", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM04", "Unique"], "modern_id": 423 }, "ITEM_HM_FLASH": { "label": "HM05 Flash", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM05", "Unique"], "modern_id": 424 }, "ITEM_HM_ROCK_SMASH": { "label": "HM06 Rock Smash", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM06", "Unique"], "modern_id": 425 }, "ITEM_HM_WATERFALL": { "label": "HM07 Waterfall", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM07", "Unique"], "modern_id": 737 }, "ITEM_HM_DIVE": { "label": "HM08 Dive", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM08", "Unique"], "modern_id": null }, @@ -375,169 +375,169 @@ "ITEM_POTION": { "label": "Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 17 }, "ITEM_ANTIDOTE": { "label": "Antidote", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 18 }, "ITEM_BURN_HEAL": { "label": "Burn Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 19 }, "ITEM_ICE_HEAL": { "label": "Ice Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 20 }, "ITEM_AWAKENING": { "label": "Awakening", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 21 }, "ITEM_PARALYZE_HEAL": { "label": "Paralyze Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 22 }, "ITEM_FULL_RESTORE": { "label": "Full Restore", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 23 }, "ITEM_MAX_POTION": { "label": "Max Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 24 }, "ITEM_HYPER_POTION": { "label": "Hyper Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 25 }, "ITEM_SUPER_POTION": { "label": "Super Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 26 }, "ITEM_FULL_HEAL": { "label": "Full Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 27 }, "ITEM_REVIVE": { "label": "Revive", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 28 }, "ITEM_MAX_REVIVE": { "label": "Max Revive", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 29 }, "ITEM_FRESH_WATER": { "label": "Fresh Water", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 30 }, "ITEM_SODA_POP": { "label": "Soda Pop", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 31 }, "ITEM_LEMONADE": { "label": "Lemonade", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 32 }, "ITEM_MOOMOO_MILK": { "label": "Moomoo Milk", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 33 }, "ITEM_ENERGY_POWDER": { "label": "Energy Powder", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 34 }, "ITEM_ENERGY_ROOT": { "label": "Energy Root", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 35 }, "ITEM_HEAL_POWDER": { "label": "Heal Powder", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 36 }, "ITEM_REVIVAL_HERB": { "label": "Revival Herb", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 37 }, "ITEM_ETHER": { "label": "Ether", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 38 }, "ITEM_MAX_ETHER": { "label": "Max Ether", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 39 }, "ITEM_ELIXIR": { "label": "Elixir", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 40 }, "ITEM_MAX_ELIXIR": { "label": "Max Elixir", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 41 }, "ITEM_LAVA_COOKIE": { "label": "Lava Cookie", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 42 }, "ITEM_BERRY_JUICE": { "label": "Berry Juice", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 43 }, "ITEM_SACRED_ASH": { "label": "Sacred Ash", "classification": "USEFUL", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 44 }, @@ -736,19 +736,19 @@ }, "ITEM_BLACK_FLUTE": { "label": "Black Flute", - "classification": "FILLER", + "classification": "USEFUL", "tags": ["Misc"], "modern_id": 68 }, "ITEM_WHITE_FLUTE": { "label": "White Flute", - "classification": "FILLER", + "classification": "USEFUL", "tags": ["Misc"], "modern_id": 69 }, "ITEM_HEART_SCALE": { "label": "Heart Scale", - "classification": "FILLER", + "classification": "USEFUL", "tags": ["Misc"], "modern_id": 93 }, @@ -757,37 +757,37 @@ "ITEM_SUN_STONE": { "label": "Sun Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 80 }, "ITEM_MOON_STONE": { "label": "Moon Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 81 }, "ITEM_FIRE_STONE": { "label": "Fire Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 82 }, "ITEM_THUNDER_STONE": { "label": "Thunder Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 83 }, "ITEM_WATER_STONE": { "label": "Water Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 84 }, "ITEM_LEAF_STONE": { "label": "Leaf Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 85 }, @@ -1215,7 +1215,7 @@ "ITEM_KINGS_ROCK": { "label": "King's Rock", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 221 }, "ITEM_SILVER_POWDER": { @@ -1245,13 +1245,13 @@ "ITEM_DEEP_SEA_TOOTH": { "label": "Deep Sea Tooth", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 226 }, "ITEM_DEEP_SEA_SCALE": { "label": "Deep Sea Scale", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 227 }, "ITEM_SMOKE_BALL": { @@ -1287,7 +1287,7 @@ "ITEM_METAL_COAT": { "label": "Metal Coat", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 233 }, "ITEM_LEFTOVERS": { @@ -1299,7 +1299,7 @@ "ITEM_DRAGON_SCALE": { "label": "Dragon Scale", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 235 }, "ITEM_LIGHT_BALL": { @@ -1401,7 +1401,7 @@ "ITEM_UP_GRADE": { "label": "Up-Grade", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 252 }, "ITEM_SHELL_BELL": { diff --git a/worlds/pokemon_emerald/data/locations.json b/worlds/pokemon_emerald/data/locations.json index 55ef15d871bb..63f42340cce4 100644 --- a/worlds/pokemon_emerald/data/locations.json +++ b/worlds/pokemon_emerald/data/locations.json @@ -1,5364 +1,6702 @@ { "BADGE_1": { "label": "Rustboro Gym - Stone Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_2": { "label": "Dewford Gym - Knuckle Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_3": { "label": "Mauville Gym - Dynamo Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_4": { "label": "Lavaridge Gym - Heat Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_5": { "label": "Petalburg Gym - Balance Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_6": { "label": "Fortree Gym - Feather Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_7": { "label": "Mossdeep Gym - Mind Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_8": { "label": "Sootopolis Gym - Rain Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "NPC_GIFT_RECEIVED_HM_CUT": { "label": "Rustboro City - HM01 from Cutter's House", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_FLY": { "label": "Route 119 - HM02 from Rival Battle", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_SURF": { "label": "Petalburg City - HM03 from Wally's Uncle", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_STRENGTH": { "label": "Rusturf Tunnel - HM04 from Tunneler", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_FLASH": { "label": "Granite Cave 1F - HM05 from Hiker", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_ROCK_SMASH": { "label": "Mauville City - HM06 from Rock Smash Guy", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_WATERFALL": { "label": "Sootopolis City - HM07 from Wallace", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_DIVE": { "label": "Mossdeep City - HM08 from Steven's House", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_ACRO_BIKE": { "label": "Mauville City - Acro Bike", - "tags": ["Bike"] + "tags": [], + "category": "BIKE" }, "NPC_GIFT_RECEIVED_MACH_BIKE": { "label": "Mauville City - Mach Bike", - "tags": ["Bike"] + "tags": [], + "category": "BIKE" }, "NPC_GIFT_RECEIVED_WAILMER_PAIL": { "label": "Route 104 - Wailmer Pail from Flower Shop Lady", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL": { "label": "Rusturf Tunnel - Recover Devon Goods", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_LETTER": { "label": "Devon Corp 3F - Letter from Mr. Stone", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_COIN_CASE": { "label": "Mauville City - Coin Case from Lady in House", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_METEORITE": { "label": "Mt Chimney - Meteorite from Machine", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_GO_GOGGLES": { "label": "Lavaridge Town - Go Goggles from Rival", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON": { "label": "Mauville City - Basement Key from Wattson", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_ITEMFINDER": { "label": "Route 110 - Itemfinder from Rival", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_DEVON_SCOPE": { "label": "Route 120 - Devon Scope from Steven", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_MAGMA_EMBLEM": { "label": "Mt Pyre Summit - Magma Emblem from Old Lady", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY": { "label": "Abandoned Ship - Captain's Office Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY": { "label": "Abandoned Ship HF - Room 4 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY": { "label": "Abandoned Ship HF - Room 1 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY": { "label": "Abandoned Ship HF - Room 6 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY": { "label": "Abandoned Ship HF - Room 2 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER": { "label": "Abandoned Ship HF - Scanner", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_POKEBLOCK_CASE": { "label": "Lilycove City - Pokeblock Case from Contest Hall", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_SS_TICKET": { "label": "Littleroot Town - S.S. Ticket from Norman", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_AURORA_TICKET": { "label": "Littleroot Town - Aurora Ticket from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_EON_TICKET": { "label": "Littleroot Town - Eon Ticket from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_MYSTIC_TICKET": { "label": "Littleroot Town - Mystic Ticket from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_OLD_SEA_MAP": { "label": "Littleroot Town - Old Sea Map from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_OLD_ROD": { "label": "Dewford Town - Old Rod from Fisherman", - "tags": ["Rod"] + "tags": [], + "category": "ROD" }, "NPC_GIFT_RECEIVED_GOOD_ROD": { "label": "Route 118 - Good Rod from Fisherman", - "tags": ["Rod"] + "tags": [], + "category": "ROD" }, "NPC_GIFT_RECEIVED_SUPER_ROD": { "label": "Mossdeep City - Super Rod from Fisherman in House", - "tags": ["Rod"] + "tags": [], + "category": "ROD" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM": { "label": "Artisan Cave B1F - Hidden Item 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON": { "label": "Artisan Cave B1F - Hidden Item 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN": { "label": "Artisan Cave B1F - Hidden Item 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC": { "label": "Artisan Cave B1F - Hidden Item 4", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_FALLARBOR_TOWN_NUGGET": { "label": "Fallarbor Town - Hidden Item in Crater", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_1": { "label": "Granite Cave B2F - Hidden Item After Crumbling Floor", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_2": { "label": "Granite Cave B2F - Hidden Item on Platform", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_JAGGED_PASS_FULL_HEAL": { "label": "Jagged Pass - Hidden Item in Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_JAGGED_PASS_GREAT_BALL": { "label": "Jagged Pass - Hidden Item in Corner", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LAVARIDGE_TOWN_ICE_HEAL": { "label": "Lavaridge Town - Hidden Item in Springs", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LILYCOVE_CITY_HEART_SCALE": { "label": "Lilycove City - Hidden Item on Beach West", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LILYCOVE_CITY_POKE_BALL": { "label": "Lilycove City - Hidden Item on Beach East", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LILYCOVE_CITY_PP_UP": { "label": "Lilycove City - Hidden Item on Beach North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_EXTERIOR_MAX_ETHER": { "label": "Mt Pyre Exterior - Hidden Item First Grave", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_EXTERIOR_ULTRA_BALL": { "label": "Mt Pyre Exterior - Hidden Item Second Grave", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_SUMMIT_RARE_CANDY": { "label": "Mt Pyre Summit - Hidden Item in Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_SUMMIT_ZINC": { "label": "Mt Pyre Summit - Hidden Item Grave", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_CITY_RARE_CANDY": { "label": "Petalburg City - Hidden Item Past Pond South", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_POKE_BALL": { "label": "Petalburg Woods - Hidden Item After Grunt", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_POTION": { "label": "Petalburg Woods - Hidden Item Southeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_1": { "label": "Petalburg Woods - Hidden Item Past Tree North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_2": { "label": "Petalburg Woods - Hidden Item Past Tree South", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_ANTIDOTE": { "label": "Route 104 - Hidden Item on Beach 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_HEART_SCALE": { "label": "Route 104 - Hidden Item on Beach 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_POTION": { "label": "Route 104 - Hidden Item on Beach 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_POKE_BALL": { "label": "Route 104 - Hidden Item Behind Flower Shop 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_SUPER_POTION": { "label": "Route 104 - Hidden Item Behind Flower Shop 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_105_BIG_PEARL": { "label": "Route 105 - Hidden Item Between Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_105_HEART_SCALE": { "label": "Route 105 - Hidden Item on Small Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_106_HEART_SCALE": { "label": "Route 106 - Hidden Item on Beach 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_106_STARDUST": { "label": "Route 106 - Hidden Item on Beach 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_106_POKE_BALL": { "label": "Route 106 - Hidden Item on Beach 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_108_RARE_CANDY": { "label": "Route 108 - Hidden Item on Rock", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_REVIVE": { "label": "Route 109 - Hidden Item on Beach Southwest", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_ETHER": { "label": "Route 109 - Hidden Item on Beach Southeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_2": { "label": "Route 109 - Hidden Item on Beach Under Umbrella", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_GREAT_BALL": { "label": "Route 109 - Hidden Item on Beach West", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_1": { "label": "Route 109 - Hidden Item on Beach Behind Old Man", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_3": { "label": "Route 109 - Hidden Item in Front of Couple", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_FULL_HEAL": { "label": "Route 110 - Hidden Item South of Rival", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_GREAT_BALL": { "label": "Route 110 - Hidden Item North of Rival", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_REVIVE": { "label": "Route 110 - Hidden Item Behind Two Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_POKE_BALL": { "label": "Route 110 - Hidden Item South of Berries", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_111_PROTEIN": { "label": "Route 111 - Hidden Item Desert Behind Tower", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_111_RARE_CANDY": { "label": "Route 111 - Hidden Item Desert on Rock 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_111_STARDUST": { "label": "Route 111 - Hidden Item Desert on Rock 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_113_ETHER": { "label": "Route 113 - Hidden Item Mound West of Three Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_113_NUGGET": { "label": "Route 113 - Hidden Item Mound Between Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_113_TM_DOUBLE_TEAM": { "label": "Route 113 - Hidden Item Mound West of Workshop", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_114_CARBOS": { "label": "Route 114 - Hidden Item Rock in Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_114_REVIVE": { "label": "Route 114 - Hidden Item West of Bridge", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_115_HEART_SCALE": { "label": "Route 115 - Hidden Item Behind Trainer on Beach", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_116_BLACK_GLASSES": { "label": "Route 116 - Hidden Item in East", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_116_SUPER_POTION": { "label": "Route 116 - Hidden Item in Tree Maze", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_117_REPEL": { "label": "Route 117 - Hidden Item Behind Flower Patch", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_118_HEART_SCALE": { "label": "Route 118 - Hidden Item West on Rock", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_118_IRON": { "label": "Route 118 - Hidden Item East on Rock", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_FULL_HEAL": { "label": "Route 119 - Hidden Item in South Tall Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_CALCIUM": { "label": "Route 119 - Hidden Item Across South Rail", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_ULTRA_BALL": { "label": "Route 119 - Hidden Item in East Tall Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_MAX_ETHER": { "label": "Route 119 - Hidden Item Next to Waterfall", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1": { "label": "Route 120 - Hidden Item Behind Trees", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_REVIVE": { "label": "Route 120 - Hidden Item in North Tall Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_ZINC": { "label": "Route 120 - Hidden Item in Tall Grass Maze", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2": { "label": "Route 120 - Hidden Item Behind Southwest Pool", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_HP_UP": { "label": "Route 121 - Hidden Item West of Grunts", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_FULL_HEAL": { "label": "Route 121 - Hidden Item in Maze 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_MAX_REVIVE": { "label": "Route 121 - Hidden Item in Maze 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_NUGGET": { "label": "Route 121 - Hidden Item Behind Tree", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_PP_UP": { "label": "Route 123 - Hidden Item East Behind Tree 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_RARE_CANDY": { "label": "Route 123 - Hidden Item East Behind Tree 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_HYPER_POTION": { "label": "Route 123 - Hidden Item on Rock Before Ledges", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_SUPER_REPEL": { "label": "Route 123 - Hidden Item in North Path Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_REVIVE": { "label": "Route 123 - Hidden Item Behind House", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_1": { "label": "Route 128 - Hidden Item North Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_2": { "label": "Route 128 - Hidden Item Center Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_3": { "label": "Route 128 - Hidden Item Southwest Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_ZINC": { "label": "Safari Zone NE - Hidden Item North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_RARE_CANDY": { "label": "Safari Zone NE - Hidden Item East", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_FULL_RESTORE": { "label": "Safari Zone SE - Hidden Item in South Grass 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_PP_UP": { "label": "Safari Zone SE - Hidden Item in South Grass 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SS_TIDAL_LOWER_DECK_LEFTOVERS": { "label": "SS Tidal - Hidden Item in Lower Deck Trash Can", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_GREEN_SHARD": { "label": "Route 124 UW - Hidden Item in Big Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_CARBOS": { "label": "Route 124 UW - Hidden Item in Tunnel Alcove", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_CALCIUM": { "label": "Route 124 UW - Hidden Item in North Tunnel 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_2": { "label": "Route 124 UW - Hidden Item in North Tunnel 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_PEARL": { "label": "Route 124 UW - Hidden Item in Small Area North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_BIG_PEARL": { "label": "Route 124 UW - Hidden Item in Small Area Middle", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_1": { "label": "Route 124 UW - Hidden Item in Small Area South", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_STARDUST": { "label": "Route 126 UW - Hidden Item Northeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_ULTRA_BALL": { "label": "Route 126 UW - Hidden Item in North Alcove", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_BIG_PEARL": { "label": "Route 126 UW - Hidden Item in Southeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_HEART_SCALE": { "label": "Route 126 UW - Hidden Item in Northwest Alcove", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_BLUE_SHARD": { "label": "Route 126 UW - Hidden Item in Southwest Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_IRON": { "label": "Route 126 UW - Hidden Item in West Area 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_PEARL": { "label": "Route 126 UW - Hidden Item in West Area 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_YELLOW_SHARD": { "label": "Route 126 UW - Hidden Item in West Area 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_STAR_PIECE": { "label": "Route 127 UW - Hidden Item in West Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_HEART_SCALE": { "label": "Route 127 UW - Hidden Item in Center Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_HP_UP": { "label": "Route 127 UW - Hidden Item in East Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_RED_SHARD": { "label": "Route 127 UW - Hidden Item in Northeast Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_128_PEARL": { "label": "Route 128 UW - Hidden Item in East Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_128_PROTEIN": { "label": "Route 128 UW - Hidden Item in Small Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_VICTORY_ROAD_1F_ULTRA_BALL": { "label": "Victory Road 1F - Hidden Item on Southeast Ledge", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_VICTORY_ROAD_B2F_ELIXIR": { "label": "Victory Road B2F - Hidden Item Above Waterfall", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_VICTORY_ROAD_B2F_MAX_REPEL": { "label": "Victory Road B2F - Hidden Item in Northeast Corner", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH": { "label": "Navel Rock Top - Hidden Item Sacred Ash", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_1_TM_RAIN_DANCE": { "label": "Abandoned Ship HF - Item in Room 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_3_WATER_STONE": { "label": "Abandoned Ship HF - Item in Room 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_6_LUXURY_BALL": { "label": "Abandoned Ship HF - Item in Room 6", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_1F_HARBOR_MAIL": { "label": "Abandoned Ship 1F - Item in East Side Northwest Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_2_1F_REVIVE": { "label": "Abandoned Ship 1F - Item in West Side North Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_B1F_ESCAPE_ROPE": { "label": "Abandoned Ship B1F - Item in South Rooms", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_B1F_TM_ICE_BEAM": { "label": "Abandoned Ship B1F - Item in Storage Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_2_B1F_DIVE_BALL": { "label": "Abandoned Ship B1F - Item in North Rooms", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B1F_MASTER_BALL": { "label": "Aqua Hideout B1F - Item in Center Room 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B1F_NUGGET": { "label": "Aqua Hideout B1F - Item in Center Room 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B1F_MAX_ELIXIR": { "label": "Aqua Hideout B1F - Item in East Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B2F_NEST_BALL": { "label": "Aqua Hideout B2F - Item in Long Hallway", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ARTISAN_CAVE_1F_CARBOS": { "label": "Artisan Cave 1F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ARTISAN_CAVE_B1F_HP_UP": { "label": "Artisan Cave B1F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_FIERY_PATH_FIRE_STONE": { "label": "Fiery Path - Item Behind Boulders 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_FIERY_PATH_TM_TOXIC": { "label": "Fiery Path - Item Behind Boulders 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_1F_ESCAPE_ROPE": { "label": "Granite Cave 1F - Item Before Ladder", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_B1F_POKE_BALL": { "label": "Granite Cave B1F - Item in Alcove", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_B2F_RARE_CANDY": { "label": "Granite Cave B2F - Item After Crumbling Floor", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_B2F_REPEL": { "label": "Granite Cave B2F - Item After Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_JAGGED_PASS_BURN_HEAL": { "label": "Jagged Pass - Item Below Hideout", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_LILYCOVE_CITY_MAX_REPEL": { "label": "Lilycove City - Item on Peninsula", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_1F_RARE_CANDY": { "label": "Magma Hideout 1F - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_2F_2R_FULL_RESTORE": { "label": "Magma Hideout 2F - Item on West Platform", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_2F_2R_MAX_ELIXIR": { "label": "Magma Hideout 2F - Item on East Platform", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_3F_1R_NUGGET": { "label": "Magma Hideout 3F - Item Before Last Floor", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_3F_2R_PP_MAX": { "label": "Magma Hideout 3F - Item in Drill Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_3F_3R_ECAPE_ROPE": { "label": "Magma Hideout 3F - Item After Groudon", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_4F_MAX_REVIVE": { "label": "Magma Hideout 4F - Item Before Groudon", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAUVILLE_CITY_X_SPEED": { "label": "Mauville City - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_FULL_HEAL": { "label": "Meteor Falls 1F - Item Northeast", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_MOON_STONE": { "label": "Meteor Falls 1F - Item West", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_PP_UP": { "label": "Meteor Falls 1F - Item Below Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_TM_IRON_TAIL": { "label": "Meteor Falls 1F - Item Before Steven's Cave", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_B1F_2R_TM_DRAGON_CLAW": { "label": "Meteor Falls B1F - Item in North Cave", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MOSSDEEP_CITY_NET_BALL": { "label": "Mossdeep City - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_2F_ULTRA_BALL": { "label": "Mt Pyre 2F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_3F_SUPER_REPEL": { "label": "Mt Pyre 3F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_4F_SEA_INCENSE": { "label": "Mt Pyre 4F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_5F_LAX_INCENSE": { "label": "Mt Pyre 5F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_6F_TM_SHADOW_BALL": { "label": "Mt Pyre 6F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_EXTERIOR_TM_SKILL_SWAP": { "label": "Mt Pyre Exterior - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_EXTERIOR_MAX_POTION": { "label": "Mt Pyre Exterior - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_ESCAPE_ROPE": { "label": "New Mauville - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_PARALYZE_HEAL": { "label": "New Mauville - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_FULL_HEAL": { "label": "New Mauville - Item 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_THUNDER_STONE": { "label": "New Mauville - Item 4", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_ULTRA_BALL": { "label": "New Mauville - Item 5", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_CITY_ETHER": { "label": "Petalburg City - Item Past Pond South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_CITY_MAX_REVIVE": { "label": "Petalburg City - Item Past Pond North", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_ETHER": { "label": "Petalburg Woods - Item Northwest", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_PARALYZE_HEAL": { "label": "Petalburg Woods - Item Southwest", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_GREAT_BALL": { "label": "Petalburg Woods - Item Past Tree Northeast", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_X_ATTACK": { "label": "Petalburg Woods - Item Past Tree South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_102_POTION": { "label": "Route 102 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_103_GUARD_SPEC": { "label": "Route 103 - Item Near Berries", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_103_PP_UP": { "label": "Route 103 - Item in Tree Maze", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_POKE_BALL": { "label": "Route 104 - Item Near Briney on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_POTION": { "label": "Route 104 - Item Behind Flower Shop", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_X_ACCURACY": { "label": "Route 104 - Item Behind Tree", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_PP_UP": { "label": "Route 104 - Item East Past Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_105_IRON": { "label": "Route 105 - Item on Island", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_106_PROTEIN": { "label": "Route 106 - Item on West Beach", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_108_STAR_PIECE": { "label": "Route 108 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_109_POTION": { "label": "Route 109 - Item on Beach", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_109_PP_UP": { "label": "Route 109 - Item on Island", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_110_DIRE_HIT": { "label": "Route 110 - Item South of Rival", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_110_ELIXIR": { "label": "Route 110 - Item South of Berries", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_110_RARE_CANDY": { "label": "Route 110 - Item on Island", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_ELIXIR": { "label": "Route 111 - Item Near Winstrates", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_HP_UP": { "label": "Route 111 - Item West of Pond Near Winstrates", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_STARDUST": { "label": "Route 111 - Item Desert Near Tower", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_TM_SANDSTORM": { "label": "Route 111 - Item Desert South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_112_NUGGET": { "label": "Route 112 - Item on Ledges", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_113_SUPER_REPEL": { "label": "Route 113 - Item Past Three Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_113_MAX_ETHER": { "label": "Route 113 - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_113_HYPER_POTION": { "label": "Route 113 - Item Near Fallarbor South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_114_ENERGY_POWDER": { "label": "Route 114 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_114_PROTEIN": { "label": "Route 114 - Item Behind Smashable Rock", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_114_RARE_CANDY": { "label": "Route 114 - Item Above Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_SUPER_POTION": { "label": "Route 115 - Item on Beach", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_PP_UP": { "label": "Route 115 - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_GREAT_BALL": { "label": "Route 115 - Item Behind Smashable Rock", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_HEAL_POWDER": { "label": "Route 115 - Item North Near Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_TM_FOCUS_PUNCH": { "label": "Route 115 - Item Near Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_IRON": { "label": "Route 115 - Item Past Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_REPEL": { "label": "Route 116 - Item in Grass", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_X_SPECIAL": { "label": "Route 116 - Item Near Tunnel", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_POTION": { "label": "Route 116 - Item in Tree Maze 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_ETHER": { "label": "Route 116 - Item in Tree Maze 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_HP_UP": { "label": "Route 116 - Item in East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_117_GREAT_BALL": { "label": "Route 117 - Item Behind Flower Patch", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_117_REVIVE": { "label": "Route 117 - Item Behind Tree", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_118_HYPER_POTION": { "label": "Route 118 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_SUPER_REPEL": { "label": "Route 119 - Item in South Tall Grass 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_HYPER_POTION_1": { "label": "Route 119 - Item in South Tall Grass 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_ZINC": { "label": "Route 119 - Item Across River South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_HYPER_POTION_2": { "label": "Route 119 - Item Near Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_ELIXIR_1": { "label": "Route 119 - Item East of Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_ELIXIR_2": { "label": "Route 119 - Item on River Bank", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_LEAF_STONE": { "label": "Route 119 - Item Near South Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_NUGGET": { "label": "Route 119 - Item Above North Waterfall 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_RARE_CANDY": { "label": "Route 119 - Item Above North Waterfall 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_NEST_BALL": { "label": "Route 120 - Item Near North Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_REVIVE": { "label": "Route 120 - Item in North Puddles", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_NUGGET": { "label": "Route 120 - Item in Tall Grass Maze", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_HYPER_POTION": { "label": "Route 120 - Item in Tall Grass South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_FULL_HEAL": { "label": "Route 120 - Item Behind Southwest Pool", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_121_ZINC": { "label": "Route 121 - Item Near Safari Zone", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_121_REVIVE": { "label": "Route 121 - Item in Maze 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_121_CARBOS": { "label": "Route 121 - Item in Maze 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_ULTRA_BALL": { "label": "Route 123 - Item Below Ledges", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_ELIXIR": { "label": "Route 123 - Item on Ledges 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_REVIVAL_HERB": { "label": "Route 123 - Item on Ledges 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_PP_UP": { "label": "Route 123 - Item on Ledges 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_CALCIUM": { "label": "Route 123 - Item on Ledges 4", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_124_RED_SHARD": { "label": "Route 124 - Item in Northwest Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_124_YELLOW_SHARD": { "label": "Route 124 - Item in Northeast Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_124_BLUE_SHARD": { "label": "Route 124 - Item in Southwest Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_125_BIG_PEARL": { "label": "Route 125 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_126_GREEN_SHARD": { "label": "Route 126 - Item in Separated Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_127_ZINC": { "label": "Route 127 - Item North", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_127_CARBOS": { "label": "Route 127 - Item East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_127_RARE_CANDY": { "label": "Route 127 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_132_PROTEIN": { "label": "Route 132 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_132_RARE_CANDY": { "label": "Route 132 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_133_BIG_PEARL": { "label": "Route 133 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_133_MAX_REVIVE": { "label": "Route 133 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_133_STAR_PIECE": { "label": "Route 133 - Item 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_134_CARBOS": { "label": "Route 134 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_134_STAR_PIECE": { "label": "Route 134 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_RUSTBORO_CITY_X_DEFEND": { "label": "Rustboro City - Item Behind Fences", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_RUSTURF_TUNNEL_POKE_BALL": { "label": "Rusturf Tunnel - Item West", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_RUSTURF_TUNNEL_MAX_ETHER": { "label": "Rusturf Tunnel - Item East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_NORTH_CALCIUM": { "label": "Safari Zone N - Item in Grass", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_NORTH_EAST_NUGGET": { "label": "Safari Zone NE - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_NORTH_WEST_TM_SOLAR_BEAM": { "label": "Safari Zone NW - Item Behind Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_SOUTH_EAST_BIG_PEARL": { "label": "Safari Zone SE - Item in Grass", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_SOUTH_WEST_MAX_REVIVE": { "label": "Safari Zone SW - Item Behind Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SCORCHED_SLAB_TM_SUNNY_DAY": { "label": "Scorched Slab - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SEAFLOOR_CAVERN_ROOM_9_TM_EARTHQUAKE": { "label": "Seafloor Cavern Room 9 - Item Before Kyogre", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_ENTRANCE_BIG_PEARL": { "label": "Shoal Cave Entrance - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_ICE_ROOM_NEVER_MELT_ICE": { "label": "Shoal Cave Ice Room - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_ICE_ROOM_TM_HAIL": { "label": "Shoal Cave Ice Room - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_INNER_ROOM_RARE_CANDY": { "label": "Shoal Cave Inner Room - Item in Center", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_STAIRS_ROOM_ICE_HEAL": { "label": "Shoal Cave Stairs Room - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL": { "label": "Trick House Puzzle 1 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL": { "label": "Trick House Puzzle 2 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL": { "label": "Trick House Puzzle 2 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL": { "label": "Trick House Puzzle 3 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL": { "label": "Trick House Puzzle 3 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL": { "label": "Trick House Puzzle 4 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL": { "label": "Trick House Puzzle 6 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL": { "label": "Trick House Puzzle 7 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL": { "label": "Trick House Puzzle 8 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_1F_MAX_ELIXIR": { "label": "Victory Road 1F - Item East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_1F_PP_UP": { "label": "Victory Road 1F - Item on Southeast Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_B1F_FULL_RESTORE": { "label": "Victory Road B1F - Item Behind Boulders", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_B1F_TM_PSYCHIC": { "label": "Victory Road B1F - Item on Northeast Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_B2F_FULL_HEAL": { "label": "Victory Road B2F - Item Above Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON": { "label": "Mauville City - TM24 from Wattson", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_6_SODA_POP": { "label": "Route 109 - Seashore House Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_AMULET_COIN": { "label": "Littleroot Town - Amulet Coin from Mom", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_CHARCOAL": { "label": "Lavaridge Town Herb Shop - Charcoal from Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_CHESTO_BERRY_ROUTE_104": { "label": "Route 104 - Gift from Woman Near Berries", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_CLEANSE_TAG": { "label": "Mt Pyre 1F - Cleanse Tag from Woman in NE Corner", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_EXP_SHARE": { "label": "Devon Corp 3F - Exp. Share from Mr. Stone", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_FOCUS_BAND": { "label": "Shoal Cave Lower Room - Focus Band from Black Belt", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_GREAT_BALL_PETALBURG_WOODS": { "label": "Petalburg Woods - Gift from Devon Employee", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_GREAT_BALL_RUSTBORO_CITY": { "label": "Rustboro City - Gift from Devon Employee", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_KINGS_ROCK": { "label": "Mossdeep City - King's Rock from Boy", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_MACHO_BRACE": { "label": "Route 111 - Winstrate Family Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_MENTAL_HERB": { "label": "Fortree City - Wingull Delivery Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_MIRACLE_SEED": { "label": "Petalburg Woods - Miracle Seed from Lady", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_POTION_OLDALE": { "label": "Oldale Town - Gift from Shop Tutorial", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_POWDER_JAR": { "label": "Slateport City - Powder Jar from Lady in Market", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_PREMIER_BALL_RUSTBORO": { "label": "Rustboro City - Gift from Boy in Apartments", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_QUICK_CLAW": { "label": "Rustboro City - Quick Claw from School Teacher", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_REPEAT_BALL": { "label": "Route 116 - Gift from Devon Researcher", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SECRET_POWER": { "label": "Route 111 - Secret Power from Man Near Tree", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SILK_SCARF": { "label": "Dewford Town - Silk Scarf from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SOFT_SAND": { "label": "Route 109 - Soft Sand from Tuber", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SOOT_SACK": { "label": "Route 113 - Soot Sack from Glass Blower", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SOOTHE_BELL": { "label": "Slateport City - Soothe Bell from Woman in Fan Club", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SUN_STONE_MOSSDEEP": { "label": "Space Center - Gift from Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_WATER_PULSE": { "label": "Sootopolis Gym - TM03 from Juan", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_CALM_MIND": { "label": "Mossdeep Gym - TM04 from Tate and Liza", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_ROAR": { "label": "Route 114 - TM05 from Roaring Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_BULK_UP": { "label": "Dewford Gym - TM08 from Brawly", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_BULLET_SEED": { "label": "Route 104 - TM09 from Boy", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_HIDDEN_POWER": { "label": "Fortree City - TM10 from Hidden Power Lady", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_GIGA_DRAIN": { "label": "Route 123 - TM19 from Girl near Berries", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_FRUSTRATION": { "label": "Pacifidlog Town - TM21 from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_RETURN": { "label": "Fallarbor Town - TM27 from Cozmo", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_RETURN_2": { "label": "Pacifidlog Town - TM27 from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_DIG": { "label": "Route 114 - TM28 from Fossil Maniac's Brother", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_BRICK_BREAK": { "label": "Sootopolis City - TM31 from Black Belt in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_SHOCK_WAVE": { "label": "Mauville Gym - TM34 from Wattson", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB": { "label": "Dewford Town - TM36 from Sludge Bomb Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_ROCK_TOMB": { "label": "Rustboro Gym - TM39 from Roxanne", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_AERIAL_ACE": { "label": "Fortree Gym - TM40 from Winona", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_TORMENT": { "label": "Slateport City - TM41 from Sailor in Battle Tent", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_FACADE": { "label": "Petalburg Gym - TM42 from Norman", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_REST": { "label": "Lilycove City - TM44 from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_ATTRACT": { "label": "Verdanturf Town - TM45 from Woman in Battle Tent", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_THIEF": { "label": "Oceanic Museum - TM46 from Aqua Grunt in Museum", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_STEEL_WING": { "label": "Granite Cave 1F - TM47 from Steven", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_SNATCH": { "label": "SS Tidal - TM49 from Thief", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_OVERHEAT": { "label": "Lavaridge Gym - TM50 from Flannery", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_WHITE_HERB": { "label": "Route 104 - White Herb from Lady Near Flower Shop", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_FLOWER_SHOP_RECEIVED_BERRY": { "label": "Route 104 - Berry from Girl in Flower Shop", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_DEEP_SEA_SCALE": { "label": "Slateport City - Deep Sea Scale from Capt. Stern", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH": { "label": "Slateport City - Deep Sea Tooth from Capt. Stern", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_1": { "label": "Trick House Puzzle 1 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_2": { "label": "Trick House Puzzle 2 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_3": { "label": "Trick House Puzzle 3 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_4": { "label": "Trick House Puzzle 4 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_5": { "label": "Trick House Puzzle 5 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_6": { "label": "Trick House Puzzle 6 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_7": { "label": "Trick House Puzzle 7 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_FIRST_POKEBALLS": { "label": "Littleroot Town - Pokeballs from Rival", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_1": { "label": "Sootopolis City - Berry from Girl on Grass 1", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_2": { "label": "Sootopolis City - Berry from Girl on Grass 2", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_ROUTE_111_RECEIVED_BERRY": { "label": "Route 111 - Berry from Girl Near Berry Trees", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_ROUTE_114_RECEIVED_BERRY": { "label": "Route 114 - Berry from Man Near House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_ROUTE_120_RECEIVED_BERRY": { "label": "Route 120 - Berry from Lady Near Berry Trees", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_1": { "label": "Route 123 - Berry from Berry Master 1", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_2": { "label": "Route 123 - Berry from Berry Master 2", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_BERRY_MASTERS_WIFE": { "label": "Route 123 - Berry from Berry Master's Wife", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_LILYCOVE_RECEIVED_BERRY": { "label": "Lilycove City - Berry from Gentleman Above Ledges", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "BERRY_TREE_01": { "label": "Route 102 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_02": { "label": "Route 102 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_03": { "label": "Route 104 - Berry Tree Flower Shop 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_04": { "label": "Route 104 - Berry Tree Flower Shop 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_05": { "label": "Route 103 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_06": { "label": "Route 103 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_07": { "label": "Route 103 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_08": { "label": "Route 104 - Berry Tree North 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_09": { "label": "Route 104 - Berry Tree North 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_10": { "label": "Route 104 - Berry Tree North 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_11": { "label": "Route 104 - Berry Tree South 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_12": { "label": "Route 104 - Berry Tree South 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_13": { "label": "Route 104 - Berry Tree South 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_14": { "label": "Route 123 - Berry Tree Berry Master 6", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_15": { "label": "Route 123 - Berry Tree Berry Master 7", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_16": { "label": "Route 110 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_17": { "label": "Route 110 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_18": { "label": "Route 110 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_19": { "label": "Route 111 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_20": { "label": "Route 111 - Berry Tree 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_21": { "label": "Route 112 - Berry Tree 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_22": { "label": "Route 112 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_23": { "label": "Route 112 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_24": { "label": "Route 112 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_25": { "label": "Route 116 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_26": { "label": "Route 116 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_27": { "label": "Route 117 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_28": { "label": "Route 117 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_29": { "label": "Route 117 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_30": { "label": "Route 123 - Berry Tree Berry Master 8", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_31": { "label": "Route 118 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_32": { "label": "Route 118 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_33": { "label": "Route 118 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_34": { "label": "Route 119 - Berry Tree North 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_35": { "label": "Route 119 - Berry Tree North 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_36": { "label": "Route 119 - Berry Tree North 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_37": { "label": "Route 120 - Berry Tree in Side Area 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_38": { "label": "Route 120 - Berry Tree in Side Area 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_39": { "label": "Route 120 - Berry Tree in Side Area 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_40": { "label": "Route 120 - Berry Tree South 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_41": { "label": "Route 120 - Berry Tree South 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_42": { "label": "Route 120 - Berry Tree South 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_43": { "label": "Route 120 - Berry Tree Pond 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_44": { "label": "Route 120 - Berry Tree Pond 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_45": { "label": "Route 120 - Berry Tree Pond 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_46": { "label": "Route 120 - Berry Tree Pond 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_47": { "label": "Route 121 - Berry Tree West 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_48": { "label": "Route 121 - Berry Tree West 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_49": { "label": "Route 121 - Berry Tree West 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_50": { "label": "Route 121 - Berry Tree West 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_51": { "label": "Route 121 - Berry Tree East 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_52": { "label": "Route 121 - Berry Tree East 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_53": { "label": "Route 121 - Berry Tree East 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_54": { "label": "Route 121 - Berry Tree East 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_55": { "label": "Route 115 - Berry Tree Behind Smashable Rock 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_56": { "label": "Route 115 - Berry Tree Behind Smashable Rock 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_57": { "label": "Route 123 - Berry Tree East 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_58": { "label": "Route 123 - Berry Tree Berry Master 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_59": { "label": "Route 123 - Berry Tree Berry Master 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_60": { "label": "Route 123 - Berry Tree Berry Master 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_61": { "label": "Route 123 - Berry Tree Berry Master 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_62": { "label": "Route 123 - Berry Tree East 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_63": { "label": "Route 123 - Berry Tree East 5", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_64": { "label": "Route 123 - Berry Tree East 6", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_65": { "label": "Route 123 - Berry Tree Berry Master 9", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_66": { "label": "Route 116 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_67": { "label": "Route 116 - Berry Tree 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_68": { "label": "Route 114 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_69": { "label": "Route 115 - Berry Tree North 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_70": { "label": "Route 115 - Berry Tree North 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_71": { "label": "Route 115 - Berry Tree North 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_72": { "label": "Route 123 - Berry Tree Berry Master 10", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_73": { "label": "Route 123 - Berry Tree Berry Master 11", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_74": { "label": "Route 123 - Berry Tree Berry Master 12", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_75": { "label": "Route 104 - Berry Tree Flower Shop 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_76": { "label": "Route 104 - Berry Tree Flower Shop 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_77": { "label": "Route 114 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_78": { "label": "Route 114 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_79": { "label": "Route 123 - Berry Tree Berry Master 5", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_80": { "label": "Route 111 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_81": { "label": "Route 111 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_82": { "label": "Route 130 - Berry Tree on Mirage Island", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_83": { "label": "Route 119 - Berry Tree Above Waterfall 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_84": { "label": "Route 119 - Berry Tree Above Waterfall 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_85": { "label": "Route 119 - Berry Tree South 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_86": { "label": "Route 119 - Berry Tree South 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_87": { "label": "Route 123 - Berry Tree East 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_88": { "label": "Route 123 - Berry Tree East 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "POKEDEX_REWARD_001": { "label": "Pokedex - Bulbasaur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_002": { "label": "Pokedex - Ivysaur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_003": { "label": "Pokedex - Venusaur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_004": { "label": "Pokedex - Charmander", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_005": { "label": "Pokedex - Charmeleon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_006": { "label": "Pokedex - Charizard", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_007": { "label": "Pokedex - Squirtle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_008": { "label": "Pokedex - Wartortle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_009": { "label": "Pokedex - Blastoise", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_010": { "label": "Pokedex - Caterpie", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_011": { "label": "Pokedex - Metapod", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_012": { "label": "Pokedex - Butterfree", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_013": { "label": "Pokedex - Weedle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_014": { "label": "Pokedex - Kakuna", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_015": { "label": "Pokedex - Beedrill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_016": { "label": "Pokedex - Pidgey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_017": { "label": "Pokedex - Pidgeotto", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_018": { "label": "Pokedex - Pidgeot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_019": { "label": "Pokedex - Rattata", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_020": { "label": "Pokedex - Raticate", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_021": { "label": "Pokedex - Spearow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_022": { "label": "Pokedex - Fearow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_023": { "label": "Pokedex - Ekans", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_024": { "label": "Pokedex - Arbok", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_025": { "label": "Pokedex - Pikachu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_026": { "label": "Pokedex - Raichu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_027": { "label": "Pokedex - Sandshrew", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_028": { "label": "Pokedex - Sandslash", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_029": { "label": "Pokedex - Nidoran Female", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_030": { "label": "Pokedex - Nidorina", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_031": { "label": "Pokedex - Nidoqueen", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_032": { "label": "Pokedex - Nidoran Male", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_033": { "label": "Pokedex - Nidorino", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_034": { "label": "Pokedex - Nidoking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_035": { "label": "Pokedex - Clefairy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_036": { "label": "Pokedex - Clefable", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_037": { "label": "Pokedex - Vulpix", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_038": { "label": "Pokedex - Ninetales", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_039": { "label": "Pokedex - Jigglypuff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_040": { "label": "Pokedex - Wigglytuff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_041": { "label": "Pokedex - Zubat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_042": { "label": "Pokedex - Golbat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_043": { "label": "Pokedex - Oddish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_044": { "label": "Pokedex - Gloom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_045": { "label": "Pokedex - Vileplume", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_046": { "label": "Pokedex - Paras", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_047": { "label": "Pokedex - Parasect", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_048": { "label": "Pokedex - Venonat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_049": { "label": "Pokedex - Venomoth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_050": { "label": "Pokedex - Diglett", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_051": { "label": "Pokedex - Dugtrio", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_052": { "label": "Pokedex - Meowth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_053": { "label": "Pokedex - Persian", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_054": { "label": "Pokedex - Psyduck", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_055": { "label": "Pokedex - Golduck", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_056": { "label": "Pokedex - Mankey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_057": { "label": "Pokedex - Primeape", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_058": { "label": "Pokedex - Growlithe", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_059": { "label": "Pokedex - Arcanine", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_060": { "label": "Pokedex - Poliwag", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_061": { "label": "Pokedex - Poliwhirl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_062": { "label": "Pokedex - Poliwrath", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_063": { "label": "Pokedex - Abra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_064": { "label": "Pokedex - Kadabra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_065": { "label": "Pokedex - Alakazam", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_066": { "label": "Pokedex - Machop", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_067": { "label": "Pokedex - Machoke", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_068": { "label": "Pokedex - Machamp", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_069": { "label": "Pokedex - Bellsprout", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_070": { "label": "Pokedex - Weepinbell", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_071": { "label": "Pokedex - Victreebel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_072": { "label": "Pokedex - Tentacool", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_073": { "label": "Pokedex - Tentacruel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_074": { "label": "Pokedex - Geodude", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_075": { "label": "Pokedex - Graveler", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_076": { "label": "Pokedex - Golem", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_077": { "label": "Pokedex - Ponyta", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_078": { "label": "Pokedex - Rapidash", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_079": { "label": "Pokedex - Slowpoke", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_080": { "label": "Pokedex - Slowbro", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_081": { "label": "Pokedex - Magnemite", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_082": { "label": "Pokedex - Magneton", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_083": { "label": "Pokedex - Farfetch'd", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_084": { "label": "Pokedex - Doduo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_085": { "label": "Pokedex - Dodrio", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_086": { "label": "Pokedex - Seel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_087": { "label": "Pokedex - Dewgong", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_088": { "label": "Pokedex - Grimer", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_089": { "label": "Pokedex - Muk", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_090": { "label": "Pokedex - Shellder", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_091": { "label": "Pokedex - Cloyster", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_092": { "label": "Pokedex - Gastly", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_093": { "label": "Pokedex - Haunter", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_094": { "label": "Pokedex - Gengar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_095": { "label": "Pokedex - Onix", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_096": { "label": "Pokedex - Drowzee", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_097": { "label": "Pokedex - Hypno", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_098": { "label": "Pokedex - Krabby", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_099": { "label": "Pokedex - Kingler", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_100": { "label": "Pokedex - Voltorb", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_101": { "label": "Pokedex - Electrode", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_102": { "label": "Pokedex - Exeggcute", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_103": { "label": "Pokedex - Exeggutor", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_104": { "label": "Pokedex - Cubone", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_105": { "label": "Pokedex - Marowak", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_106": { "label": "Pokedex - Hitmonlee", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_107": { "label": "Pokedex - Hitmonchan", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_108": { "label": "Pokedex - Lickitung", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_109": { "label": "Pokedex - Koffing", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_110": { "label": "Pokedex - Weezing", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_111": { "label": "Pokedex - Rhyhorn", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_112": { "label": "Pokedex - Rhydon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_113": { "label": "Pokedex - Chansey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_114": { "label": "Pokedex - Tangela", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_115": { "label": "Pokedex - Kangaskhan", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_116": { "label": "Pokedex - Horsea", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_117": { "label": "Pokedex - Seadra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_118": { "label": "Pokedex - Goldeen", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_119": { "label": "Pokedex - Seaking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_120": { "label": "Pokedex - Staryu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_121": { "label": "Pokedex - Starmie", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_122": { "label": "Pokedex - Mr. Mime", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_123": { "label": "Pokedex - Scyther", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_124": { "label": "Pokedex - Jynx", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_125": { "label": "Pokedex - Electabuzz", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_126": { "label": "Pokedex - Magmar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_127": { "label": "Pokedex - Pinsir", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_128": { "label": "Pokedex - Tauros", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_129": { "label": "Pokedex - Magikarp", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_130": { "label": "Pokedex - Gyarados", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_131": { "label": "Pokedex - Lapras", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_132": { "label": "Pokedex - Ditto", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_133": { "label": "Pokedex - Eevee", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_134": { "label": "Pokedex - Vaporeon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_135": { "label": "Pokedex - Jolteon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_136": { "label": "Pokedex - Flareon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_137": { "label": "Pokedex - Porygon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_138": { "label": "Pokedex - Omanyte", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_139": { "label": "Pokedex - Omastar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_140": { "label": "Pokedex - Kabuto", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_141": { "label": "Pokedex - Kabutops", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_142": { "label": "Pokedex - Aerodactyl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_143": { "label": "Pokedex - Snorlax", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_144": { "label": "Pokedex - Articuno", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_145": { "label": "Pokedex - Zapdos", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_146": { "label": "Pokedex - Moltres", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_147": { "label": "Pokedex - Dratini", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_148": { "label": "Pokedex - Dragonair", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_149": { "label": "Pokedex - Dragonite", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_150": { "label": "Pokedex - Mewtwo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_151": { "label": "Pokedex - Mew", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_152": { "label": "Pokedex - Chikorita", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_153": { "label": "Pokedex - Bayleef", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_154": { "label": "Pokedex - Meganium", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_155": { "label": "Pokedex - Cyndaquil", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_156": { "label": "Pokedex - Quilava", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_157": { "label": "Pokedex - Typhlosion", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_158": { "label": "Pokedex - Totodile", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_159": { "label": "Pokedex - Croconaw", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_160": { "label": "Pokedex - Feraligatr", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_161": { "label": "Pokedex - Sentret", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_162": { "label": "Pokedex - Furret", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_163": { "label": "Pokedex - Hoothoot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_164": { "label": "Pokedex - Noctowl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_165": { "label": "Pokedex - Ledyba", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_166": { "label": "Pokedex - Ledian", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_167": { "label": "Pokedex - Spinarak", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_168": { "label": "Pokedex - Ariados", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_169": { "label": "Pokedex - Crobat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_170": { "label": "Pokedex - Chinchou", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_171": { "label": "Pokedex - Lanturn", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_172": { "label": "Pokedex - Pichu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_173": { "label": "Pokedex - Cleffa", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_174": { "label": "Pokedex - Igglybuff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_175": { "label": "Pokedex - Togepi", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_176": { "label": "Pokedex - Togetic", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_177": { "label": "Pokedex - Natu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_178": { "label": "Pokedex - Xatu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_179": { "label": "Pokedex - Mareep", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_180": { "label": "Pokedex - Flaaffy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_181": { "label": "Pokedex - Ampharos", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_182": { "label": "Pokedex - Bellossom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_183": { "label": "Pokedex - Marill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_184": { "label": "Pokedex - Azumarill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_185": { "label": "Pokedex - Sudowoodo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_186": { "label": "Pokedex - Politoed", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_187": { "label": "Pokedex - Hoppip", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_188": { "label": "Pokedex - Skiploom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_189": { "label": "Pokedex - Jumpluff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_190": { "label": "Pokedex - Aipom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_191": { "label": "Pokedex - Sunkern", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_192": { "label": "Pokedex - Sunflora", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_193": { "label": "Pokedex - Yanma", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_194": { "label": "Pokedex - Wooper", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_195": { "label": "Pokedex - Quagsire", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_196": { "label": "Pokedex - Espeon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_197": { "label": "Pokedex - Umbreon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_198": { "label": "Pokedex - Murkrow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_199": { "label": "Pokedex - Slowking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_200": { "label": "Pokedex - Misdreavus", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_201": { "label": "Pokedex - Unown", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_202": { "label": "Pokedex - Wobbuffet", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_203": { "label": "Pokedex - Girafarig", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_204": { "label": "Pokedex - Pineco", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_205": { "label": "Pokedex - Forretress", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_206": { "label": "Pokedex - Dunsparce", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_207": { "label": "Pokedex - Gligar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_208": { "label": "Pokedex - Steelix", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_209": { "label": "Pokedex - Snubbull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_210": { "label": "Pokedex - Granbull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_211": { "label": "Pokedex - Qwilfish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_212": { "label": "Pokedex - Scizor", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_213": { "label": "Pokedex - Shuckle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_214": { "label": "Pokedex - Heracross", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_215": { "label": "Pokedex - Sneasel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_216": { "label": "Pokedex - Teddiursa", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_217": { "label": "Pokedex - Ursaring", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_218": { "label": "Pokedex - Slugma", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_219": { "label": "Pokedex - Magcargo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_220": { "label": "Pokedex - Swinub", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_221": { "label": "Pokedex - Piloswine", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_222": { "label": "Pokedex - Corsola", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_223": { "label": "Pokedex - Remoraid", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_224": { "label": "Pokedex - Octillery", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_225": { "label": "Pokedex - Delibird", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_226": { "label": "Pokedex - Mantine", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_227": { "label": "Pokedex - Skarmory", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_228": { "label": "Pokedex - Houndour", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_229": { "label": "Pokedex - Houndoom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_230": { "label": "Pokedex - Kingdra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_231": { "label": "Pokedex - Phanpy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_232": { "label": "Pokedex - Donphan", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_233": { "label": "Pokedex - Porygon2", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_234": { "label": "Pokedex - Stantler", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_235": { "label": "Pokedex - Smeargle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_236": { "label": "Pokedex - Tyrogue", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_237": { "label": "Pokedex - Hitmontop", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_238": { "label": "Pokedex - Smoochum", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_239": { "label": "Pokedex - Elekid", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_240": { "label": "Pokedex - Magby", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_241": { "label": "Pokedex - Miltank", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_242": { "label": "Pokedex - Blissey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_243": { "label": "Pokedex - Raikou", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_244": { "label": "Pokedex - Entei", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_245": { "label": "Pokedex - Suicune", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_246": { "label": "Pokedex - Larvitar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_247": { "label": "Pokedex - Pupitar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_248": { "label": "Pokedex - Tyranitar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_249": { "label": "Pokedex - Lugia", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_250": { "label": "Pokedex - Ho-Oh", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_251": { "label": "Pokedex - Celebi", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_252": { "label": "Pokedex - Treecko", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_253": { "label": "Pokedex - Grovyle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_254": { "label": "Pokedex - Sceptile", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_255": { "label": "Pokedex - Torchic", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_256": { "label": "Pokedex - Combusken", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_257": { "label": "Pokedex - Blaziken", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_258": { "label": "Pokedex - Mudkip", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_259": { "label": "Pokedex - Marshtomp", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_260": { "label": "Pokedex - Swampert", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_261": { "label": "Pokedex - Poochyena", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_262": { "label": "Pokedex - Mightyena", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_263": { "label": "Pokedex - Zigzagoon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_264": { "label": "Pokedex - Linoone", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_265": { "label": "Pokedex - Wurmple", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_266": { "label": "Pokedex - Silcoon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_267": { "label": "Pokedex - Beautifly", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_268": { "label": "Pokedex - Cascoon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_269": { "label": "Pokedex - Dustox", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_270": { "label": "Pokedex - Lotad", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_271": { "label": "Pokedex - Lombre", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_272": { "label": "Pokedex - Ludicolo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_273": { "label": "Pokedex - Seedot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_274": { "label": "Pokedex - Nuzleaf", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_275": { "label": "Pokedex - Shiftry", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_276": { "label": "Pokedex - Taillow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_277": { "label": "Pokedex - Swellow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_278": { "label": "Pokedex - Wingull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_279": { "label": "Pokedex - Pelipper", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_280": { "label": "Pokedex - Ralts", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_281": { "label": "Pokedex - Kirlia", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_282": { "label": "Pokedex - Gardevoir", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_283": { "label": "Pokedex - Surskit", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_284": { "label": "Pokedex - Masquerain", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_285": { "label": "Pokedex - Shroomish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_286": { "label": "Pokedex - Breloom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_287": { "label": "Pokedex - Slakoth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_288": { "label": "Pokedex - Vigoroth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_289": { "label": "Pokedex - Slaking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_290": { "label": "Pokedex - Nincada", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_291": { "label": "Pokedex - Ninjask", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_292": { "label": "Pokedex - Shedinja", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_293": { "label": "Pokedex - Whismur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_294": { "label": "Pokedex - Loudred", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_295": { "label": "Pokedex - Exploud", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_296": { "label": "Pokedex - Makuhita", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_297": { "label": "Pokedex - Hariyama", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_298": { "label": "Pokedex - Azurill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_299": { "label": "Pokedex - Nosepass", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_300": { "label": "Pokedex - Skitty", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_301": { "label": "Pokedex - Delcatty", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_302": { "label": "Pokedex - Sableye", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_303": { "label": "Pokedex - Mawile", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_304": { "label": "Pokedex - Aron", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_305": { "label": "Pokedex - Lairon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_306": { "label": "Pokedex - Aggron", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_307": { "label": "Pokedex - Meditite", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_308": { "label": "Pokedex - Medicham", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_309": { "label": "Pokedex - Electrike", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_310": { "label": "Pokedex - Manectric", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_311": { "label": "Pokedex - Plusle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_312": { "label": "Pokedex - Minun", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_313": { "label": "Pokedex - Volbeat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_314": { "label": "Pokedex - Illumise", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_315": { "label": "Pokedex - Roselia", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_316": { "label": "Pokedex - Gulpin", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_317": { "label": "Pokedex - Swalot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_318": { "label": "Pokedex - Carvanha", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_319": { "label": "Pokedex - Sharpedo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_320": { "label": "Pokedex - Wailmer", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_321": { "label": "Pokedex - Wailord", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_322": { "label": "Pokedex - Numel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_323": { "label": "Pokedex - Camerupt", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_324": { "label": "Pokedex - Torkoal", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_325": { "label": "Pokedex - Spoink", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_326": { "label": "Pokedex - Grumpig", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_327": { "label": "Pokedex - Spinda", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_328": { "label": "Pokedex - Trapinch", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_329": { "label": "Pokedex - Vibrava", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_330": { "label": "Pokedex - Flygon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_331": { "label": "Pokedex - Cacnea", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_332": { "label": "Pokedex - Cacturne", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_333": { "label": "Pokedex - Swablu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_334": { "label": "Pokedex - Altaria", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_335": { "label": "Pokedex - Zangoose", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_336": { "label": "Pokedex - Seviper", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_337": { "label": "Pokedex - Lunatone", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_338": { "label": "Pokedex - Solrock", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_339": { "label": "Pokedex - Barboach", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_340": { "label": "Pokedex - Whiscash", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_341": { "label": "Pokedex - Corphish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_342": { "label": "Pokedex - Crawdaunt", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_343": { "label": "Pokedex - Baltoy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_344": { "label": "Pokedex - Claydol", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_345": { "label": "Pokedex - Lileep", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_346": { "label": "Pokedex - Cradily", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_347": { "label": "Pokedex - Anorith", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_348": { "label": "Pokedex - Armaldo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_349": { "label": "Pokedex - Feebas", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_350": { "label": "Pokedex - Milotic", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_351": { "label": "Pokedex - Castform", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_352": { "label": "Pokedex - Kecleon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_353": { "label": "Pokedex - Shuppet", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_354": { "label": "Pokedex - Banette", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_355": { "label": "Pokedex - Duskull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_356": { "label": "Pokedex - Dusclops", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_357": { "label": "Pokedex - Tropius", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_358": { "label": "Pokedex - Chimecho", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_359": { "label": "Pokedex - Absol", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_360": { "label": "Pokedex - Wynaut", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_361": { "label": "Pokedex - Snorunt", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_362": { "label": "Pokedex - Glalie", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_363": { "label": "Pokedex - Spheal", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_364": { "label": "Pokedex - Sealeo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_365": { "label": "Pokedex - Walrein", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_366": { "label": "Pokedex - Clamperl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_367": { "label": "Pokedex - Huntail", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_368": { "label": "Pokedex - Gorebyss", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_369": { "label": "Pokedex - Relicanth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_370": { "label": "Pokedex - Luvdisc", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_371": { "label": "Pokedex - Bagon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_372": { "label": "Pokedex - Shelgon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_373": { "label": "Pokedex - Salamence", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_374": { "label": "Pokedex - Beldum", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_375": { "label": "Pokedex - Metang", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_376": { "label": "Pokedex - Metagross", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_377": { "label": "Pokedex - Regirock", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_378": { "label": "Pokedex - Regice", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_379": { "label": "Pokedex - Registeel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_380": { "label": "Pokedex - Latias", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_381": { "label": "Pokedex - Latios", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_382": { "label": "Pokedex - Kyogre", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_383": { "label": "Pokedex - Groudon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_384": { "label": "Pokedex - Rayquaza", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_385": { "label": "Pokedex - Jirachi", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_386": { "label": "Pokedex - Deoxys", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "TRAINER_AARON_REWARD": { "label": "Route 134 - Dragon Tamer Aaron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ABIGAIL_1_REWARD": { "label": "Route 110 - Triathlete Abigail", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AIDAN_REWARD": { "label": "Route 127 - Bird Keeper Aidan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AISHA_REWARD": { "label": "Route 117 - Battle Girl Aisha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALBERTO_REWARD": { "label": "Route 123 - Bird Keeper Alberto", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALBERT_REWARD": { "label": "Victory Road 1F - Cooltrainer Albert", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALEXA_REWARD": { "label": "Route 128 - Cooltrainer Alexa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALEXIA_REWARD": { "label": "Petalburg Gym - Cooltrainer Alexia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALEX_REWARD": { "label": "Route 134 - Bird Keeper Alex", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALICE_REWARD": { "label": "Route 109 - Swimmer Alice", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALIX_REWARD": { "label": "Route 115 - Psychic Alix", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALLEN_REWARD": { "label": "Route 102 - Youngster Allen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALLISON_REWARD": { "label": "Route 129 - Triathlete Allison", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALYSSA_REWARD": { "label": "Route 110 - Triathlete Alyssa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AMY_AND_LIV_1_REWARD": { "label": "Route 103 - Twins Amy and Liv", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANNA_AND_MEG_1_REWARD": { "label": "Route 117 - Sr. and Jr. Anna and Meg", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANDREA_REWARD": { "label": "Sootopolis Gym - Lass Andrea", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANDRES_1_REWARD": { "label": "Route 105 - Ruin Maniac Andres", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANDREW_REWARD": { "label": "Route 103 - Fisherman Andrew", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANGELICA_REWARD": { "label": "Route 120 - Parasol Lady Angelica", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANGELINA_REWARD": { "label": "Route 114 - Picnicker Angelina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANGELO_REWARD": { "label": "Mauville Gym - Bug Maniac Angelo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANNIKA_REWARD": { "label": "Sootopolis Gym - Pokefan Annika", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANTHONY_REWARD": { "label": "Route 110 - Triathlete Anthony", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ARCHIE_REWARD": { "label": "Seafloor Cavern Room 9 - Aqua Leader Archie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ASHLEY_REWARD": { "label": "Fortree Gym - Picnicker Ashley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ATHENA_REWARD": { "label": "Route 127 - Cooltrainer Athena", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ATSUSHI_REWARD": { "label": "Mt Pyre 5F - Black Belt Atsushi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AURON_REWARD": { "label": "Route 125 - Expert Auron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AUSTINA_REWARD": { "label": "Route 109 - Tuber Austina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AUTUMN_REWARD": { "label": "Jagged Pass - Picnicker Autumn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AXLE_REWARD": { "label": "Lavaridge Gym - Kindler Axle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BARNY_REWARD": { "label": "Route 118 - Fisherman Barny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BARRY_REWARD": { "label": "Route 126 - Swimmer Barry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BEAU_REWARD": { "label": "Route 111 - Camper Beau", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BECKY_REWARD": { "label": "Route 111 - Picnicker Becky", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BECK_REWARD": { "label": "Route 133 - Bird Keeper Beck", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BENJAMIN_1_REWARD": { "label": "Route 110 - Triathlete Benjamin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BEN_REWARD": { "label": "Mauville Gym - Youngster Ben", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BERKE_REWARD": { "label": "Petalburg Gym - Cooltrainer Berke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BERNIE_1_REWARD": { "label": "Route 114 - Kindler Bernie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BETHANY_REWARD": { "label": "Sootopolis Gym - Pokefan Bethany", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BETH_REWARD": { "label": "Route 107 - Swimmer Beth", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BEVERLY_REWARD": { "label": "Route 105 - Swimmer Beverly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BIANCA_REWARD": { "label": "Route 111 - Picnicker Bianca", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BILLY_REWARD": { "label": "Route 104 - Youngster Billy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BLAKE_REWARD": { "label": "Mossdeep Gym - Psychic Blake", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRANDEN_REWARD": { "label": "Route 111 - Camper Branden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRANDI_REWARD": { "label": "Route 117 - Psychic Brandi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRAWLY_1_REWARD": { "label": "Dewford Gym - Leader Brawly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRAXTON_REWARD": { "label": "Route 123 - Cooltrainer Braxton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_LILYCOVE_MUDKIP_REWARD": { "label": "Lilycove City - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_ROUTE_103_MUDKIP_REWARD": { "label": "Route 103 - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_ROUTE_110_MUDKIP_REWARD": { "label": "Route 110 - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_ROUTE_119_MUDKIP_REWARD": { "label": "Route 119 - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD": { "label": "Rustboro City - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDA_REWARD": { "label": "Route 126 - Swimmer Brenda", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDEN_REWARD": { "label": "Dewford Gym - Sailor Brenden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENT_REWARD": { "label": "Route 119 - Bug Maniac Brent", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRIANNA_REWARD": { "label": "Sootopolis Gym - Lady Brianna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRICE_REWARD": { "label": "Route 112 - Hiker Brice", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRIDGET_REWARD": { "label": "Sootopolis Gym - Beauty Bridget", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BROOKE_1_REWARD": { "label": "Route 111 - Cooltrainer Brooke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRYANT_REWARD": { "label": "Route 112 - Kindler Bryant", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRYAN_REWARD": { "label": "Route 111 - Ruin Maniac Bryan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CALE_REWARD": { "label": "Route 121 - Bug Maniac Cale", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CALLIE_REWARD": { "label": "Route 120 - Battle Girl Callie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CALVIN_1_REWARD": { "label": "Route 102 - Youngster Calvin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAMDEN_REWARD": { "label": "Route 127 - Triathlete Camden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAMERON_1_REWARD": { "label": "Route 123 - Psychic Cameron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAMRON_REWARD": { "label": "Route 107 - Triathlete Camron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CARLEE_REWARD": { "label": "Route 128 - Swimmer Carlee", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAROLINA_REWARD": { "label": "Route 108 - Cooltrainer Carolina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAROLINE_REWARD": { "label": "Victory Road B2F - Cooltrainer Caroline", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAROL_REWARD": { "label": "Route 112 - Picnicker Carol", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CARTER_REWARD": { "label": "Route 109 - Fisherman Carter", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CATHERINE_1_REWARD": { "label": "Route 119 - Pokemon Ranger Catherine", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CEDRIC_REWARD": { "label": "Mt Pyre 6F - Psychic Cedric", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CELIA_REWARD": { "label": "Route 111 - Picnicker Celia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CELINA_REWARD": { "label": "Route 111 - Aroma Lady Celina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHAD_REWARD": { "label": "Route 124 - Swimmer Chad", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHANDLER_REWARD": { "label": "Route 109 - Tuber Chandler", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHARLIE_REWARD": { "label": "Abandoned Ship 1F - Tuber Charlie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHARLOTTE_REWARD": { "label": "Route 114 - Picnicker Charlotte", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHASE_REWARD": { "label": "Route 129 - Triathlete Chase", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHESTER_REWARD": { "label": "Route 118 - Bird Keeper Chester", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHIP_REWARD": { "label": "Route 120 - Ruin Maniac Chip", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHRIS_REWARD": { "label": "Route 119 - Fisherman Chris", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CINDY_1_REWARD": { "label": "Route 104 - Lady Cindy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLARENCE_REWARD": { "label": "Route 129 - Swimmer Clarence", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLARISSA_REWARD": { "label": "Route 120 - Parasol Lady Clarissa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLARK_REWARD": { "label": "Route 116 - Hiker Clark", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLAUDE_REWARD": { "label": "Route 114 - Fisherman Claude", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLIFFORD_REWARD": { "label": "Mossdeep Gym - Gentleman Clifford", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COBY_REWARD": { "label": "Route 113 - Bird Keeper Coby", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COLE_REWARD": { "label": "Lavaridge Gym - Kindler Cole", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COLIN_REWARD": { "label": "Route 120 - Bird Keeper Colin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COLTON_REWARD": { "label": "SS Tidal - Pokefan Colton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CONNIE_REWARD": { "label": "Sootopolis Gym - Beauty Connie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CONOR_REWARD": { "label": "Route 133 - Expert Conor", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CORY_1_REWARD": { "label": "Route 108 - Sailor Cory", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CRISSY_REWARD": { "label": "Sootopolis Gym - Lass Crissy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CRISTIAN_REWARD": { "label": "Dewford Gym - Black Belt Cristian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CRISTIN_1_REWARD": { "label": "Route 121 - Cooltrainer Cristin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CYNDY_1_REWARD": { "label": "Route 115 - Battle Girl Cyndy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAISUKE_REWARD": { "label": "Route 111 - Black Belt Daisuke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAISY_REWARD": { "label": "Route 103 - Aroma Lady Daisy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DALE_REWARD": { "label": "Route 110 - Fisherman Dale", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DALTON_1_REWARD": { "label": "Route 118 - Guitarist Dalton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DANA_REWARD": { "label": "Route 132 - Swimmer Dana", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DANIELLE_REWARD": { "label": "Lavaridge Gym - Battle Girl Danielle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAPHNE_REWARD": { "label": "Sootopolis Gym - Lady Daphne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARCY_REWARD": { "label": "Route 132 - Cooltrainer Darcy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARIAN_REWARD": { "label": "Route 104 - Fisherman Darian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARIUS_REWARD": { "label": "Fortree Gym - Bird Keeper Darius", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARRIN_REWARD": { "label": "Route 107 - Swimmer Darrin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAVID_REWARD": { "label": "Route 109 - Swimmer David", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAVIS_REWARD": { "label": "Route 123 - Bug Catcher Davis", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAWSON_REWARD": { "label": "Route 116 - Rich Boy Dawson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAYTON_REWARD": { "label": "Route 119 - Kindler Dayton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEANDRE_REWARD": { "label": "Route 118 - Youngster Deandre", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEAN_REWARD": { "label": "Route 126 - Swimmer Dean", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEBRA_REWARD": { "label": "Route 133 - Swimmer Debra", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DECLAN_REWARD": { "label": "Route 124 - Swimmer Declan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEMETRIUS_REWARD": { "label": "Abandoned Ship 1F - Youngster Demetrius", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DENISE_REWARD": { "label": "Route 107 - Swimmer Denise", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEREK_REWARD": { "label": "Route 117 - Bug Maniac Derek", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEVAN_REWARD": { "label": "Route 116 - Hiker Devan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DIANA_1_REWARD": { "label": "Jagged Pass - Picnicker Diana", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DIANNE_REWARD": { "label": "Victory Road B2F - Cooltrainer Dianne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DILLON_REWARD": { "label": "Route 113 - Youngster Dillon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DOMINIK_REWARD": { "label": "Route 105 - Swimmer Dominik", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DONALD_REWARD": { "label": "Route 119 - Bug Maniac Donald", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DONNY_REWARD": { "label": "Route 127 - Triathlete Donny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DOUGLAS_REWARD": { "label": "Route 106 - Swimmer Douglas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DOUG_REWARD": { "label": "Route 119 - Bug Catcher Doug", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DRAKE_REWARD": { "label": "Ever Grande City - Elite Four Drake", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DREW_REWARD": { "label": "Route 111 - Camper Drew", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DUNCAN_REWARD": { "label": "Abandoned Ship B1F - Sailor Duncan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DUSTY_1_REWARD": { "label": "Route 111 - Ruin Maniac Dusty", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DWAYNE_REWARD": { "label": "Route 109 - Sailor Dwayne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DYLAN_1_REWARD": { "label": "Route 117 - Triathlete Dylan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEZ_AND_LUKE_REWARD": { "label": "Mt Pyre 2F - Young Couple Dez and Luke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDGAR_REWARD": { "label": "Victory Road 1F - Cooltrainer Edgar", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDMOND_REWARD": { "label": "Route 109 - Sailor Edmond", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDWARDO_REWARD": { "label": "Fortree Gym - Bird Keeper Edwardo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDWARD_REWARD": { "label": "Route 110 - Psychic Edward", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDWIN_1_REWARD": { "label": "Route 110 - Collector Edwin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ED_REWARD": { "label": "Route 123 - Collector Ed", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ELIJAH_REWARD": { "label": "Route 109 - Bird Keeper Elijah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ELI_REWARD": { "label": "Lavaridge Gym - Hiker Eli", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ELLIOT_1_REWARD": { "label": "Route 106 - Fisherman Elliot", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ERIC_REWARD": { "label": "Jagged Pass - Hiker Eric", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ERNEST_1_REWARD": { "label": "Route 125 - Sailor Ernest", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ETHAN_1_REWARD": { "label": "Jagged Pass - Camper Ethan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FABIAN_REWARD": { "label": "Route 119 - Guitarist Fabian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FELIX_REWARD": { "label": "Victory Road B2F - Cooltrainer Felix", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FERNANDO_1_REWARD": { "label": "Route 123 - Guitarist Fernando", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FLANNERY_1_REWARD": { "label": "Lavaridge Gym - Leader Flannery", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FLINT_REWARD": { "label": "Fortree Gym - Camper Flint", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FOSTER_REWARD": { "label": "Route 105 - Ruin Maniac Foster", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FRANKLIN_REWARD": { "label": "Route 133 - Swimmer Franklin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FREDRICK_REWARD": { "label": "Route 123 - Expert Fredrick", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GABRIELLE_1_REWARD": { "label": "Mt Pyre 3F - Pokemon Breeder Gabrielle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GARRET_REWARD": { "label": "SS Tidal - Rich Boy Garret", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GARRISON_REWARD": { "label": "Abandoned Ship 1F - Ruin Maniac Garrison", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GEORGE_REWARD": { "label": "Petalburg Gym - Cooltrainer George", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GERALD_REWARD": { "label": "Lavaridge Gym - Cooltrainer Gerald", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GILBERT_REWARD": { "label": "Route 132 - Swimmer Gilbert", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GINA_AND_MIA_1_REWARD": { "label": "Route 104 - Twins Gina and Mia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GLACIA_REWARD": { "label": "Ever Grande City - Elite Four Glacia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRACE_REWARD": { "label": "Route 124 - Swimmer Grace", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GREG_REWARD": { "label": "Route 119 - Bug Catcher Greg", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_1_REWARD": { "label": "Aqua Hideout 1F - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_2_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_3_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_4_REWARD": { "label": "Aqua Hideout B2F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_5_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_6_REWARD": { "label": "Aqua Hideout B2F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_7_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_8_REWARD": { "label": "Aqua Hideout B2F - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_10_REWARD": { "label": "Magma Hideout 3F - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_11_REWARD": { "label": "Magma Hideout 4F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_12_REWARD": { "label": "Magma Hideout 4F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_13_REWARD": { "label": "Magma Hideout 4F - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_14_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_15_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 5", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_16_REWARD": { "label": "Magma Hideout 3F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_1_REWARD": { "label": "Magma Hideout 1F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_2_REWARD": { "label": "Magma Hideout 1F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_3_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_4_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_5_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_6_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 6", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_7_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 7", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_8_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 8", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_9_REWARD": { "label": "Magma Hideout 3F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_CHIMNEY_1_REWARD": { "label": "Mt Chimney - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_CHIMNEY_2_REWARD": { "label": "Mt Chimney - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_1_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_2_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_3_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_4_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MUSEUM_1_REWARD": { "label": "Oceanic Museum - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MUSEUM_2_REWARD": { "label": "Oceanic Museum - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_PETALBURG_WOODS_REWARD": { "label": "Petalburg Woods - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_RUSTURF_TUNNEL_REWARD": { "label": "Rusturf Tunnel - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_1_REWARD": { "label": "Seafloor Cavern Room 1 - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_2_REWARD": { "label": "Seafloor Cavern Room 1 - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_3_REWARD": { "label": "Seafloor Cavern Room 4 - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_4_REWARD": { "label": "Seafloor Cavern Room 4 - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_5_REWARD": { "label": "Seafloor Cavern Room 3 - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_1_REWARD": { "label": "Space Center - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_2_REWARD": { "label": "Space Center - Team Magma Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_3_REWARD": { "label": "Space Center - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_4_REWARD": { "label": "Space Center - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_5_REWARD": { "label": "Space Center - Team Magma Grunt 5", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_6_REWARD": { "label": "Space Center - Team Magma Grunt 6", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_7_REWARD": { "label": "Space Center - Team Magma Grunt 7", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_1_REWARD": { "label": "Weather Institute 1F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_2_REWARD": { "label": "Weather Institute 2F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_3_REWARD": { "label": "Weather Institute 2F - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_4_REWARD": { "label": "Weather Institute 1F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_5_REWARD": { "label": "Weather Institute 2F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GWEN_REWARD": { "label": "Route 109 - Tuber Gwen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HAILEY_REWARD": { "label": "Route 109 - Tuber Hailey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HALEY_1_REWARD": { "label": "Route 104 - Lass Haley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HALLE_REWARD": { "label": "Victory Road B1F - Cooltrainer Halle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HANNAH_REWARD": { "label": "Mossdeep Gym - Psychic Hannah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HARRISON_REWARD": { "label": "Route 128 - Swimmer Harrison", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HAYDEN_REWARD": { "label": "Route 111 - Kindler Hayden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HECTOR_REWARD": { "label": "Route 115 - Collector Hector", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HEIDI_REWARD": { "label": "Route 111 - Picnicker Heidi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HELENE_REWARD": { "label": "Route 115 - Battle Girl Helene", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HENRY_REWARD": { "label": "Route 127 - Fisherman Henry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HERMAN_REWARD": { "label": "Route 131 - Swimmer Herman", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HIDEO_REWARD": { "label": "Route 119 - Ninja Boy Hideo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HITOSHI_REWARD": { "label": "Route 134 - Black Belt Hitoshi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HOPE_REWARD": { "label": "Victory Road 1F - Cooltrainer Hope", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUDSON_REWARD": { "label": "Route 134 - Sailor Hudson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUEY_REWARD": { "label": "Route 109 - Sailor Huey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUGH_REWARD": { "label": "Route 119 - Bird Keeper Hugh", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUMBERTO_REWARD": { "label": "Fortree Gym - Bird Keeper Humberto", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_IMANI_REWARD": { "label": "Route 105 - Swimmer Imani", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_IRENE_REWARD": { "label": "Route 111 - Picnicker Irene", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISAAC_1_REWARD": { "label": "Route 117 - Pokemon Breeder Isaac", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISABELLA_REWARD": { "label": "Route 124 - Triathlete Isabella", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISABELLE_REWARD": { "label": "Route 103 - Swimmer Isabelle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISABEL_1_REWARD": { "label": "Route 110 - Pokefan Isabel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISAIAH_1_REWARD": { "label": "Route 128 - Triathlete Isaiah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISOBEL_REWARD": { "label": "Route 126 - Triathlete Isobel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_IVAN_REWARD": { "label": "Route 104 - Fisherman Ivan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACE_REWARD": { "label": "Lavaridge Gym - Kindler Jace", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACKI_1_REWARD": { "label": "Route 123 - Psychic Jacki", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACKSON_1_REWARD": { "label": "Route 119 - Pokemon Ranger Jackson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACK_REWARD": { "label": "Route 134 - Swimmer Jack", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACLYN_REWARD": { "label": "Route 110 - Psychic Jaclyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACOB_REWARD": { "label": "Route 110 - Triathlete Jacob", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAIDEN_REWARD": { "label": "Route 115 - Ninja Boy Jaiden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAMES_1_REWARD": { "label": "Petalburg Woods - Bug Catcher James", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JANICE_REWARD": { "label": "Route 116 - Lass Janice", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JANI_REWARD": { "label": "Abandoned Ship 1F - Tuber Jani", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JARED_REWARD": { "label": "Fortree Gym - Bird Keeper Jared", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JASMINE_REWARD": { "label": "Route 110 - Triathlete Jasmine", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAYLEN_REWARD": { "label": "Route 113 - Youngster Jaylen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAZMYN_REWARD": { "label": "Route 123 - Cooltrainer Jazmyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JEFFREY_1_REWARD": { "label": "Route 120 - Bug Maniac Jeffrey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JEFF_REWARD": { "label": "Lavaridge Gym - Kindler Jeff", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JENNA_REWARD": { "label": "Route 120 - Pokemon Ranger Jenna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JENNIFER_REWARD": { "label": "Route 120 - Cooltrainer Jennifer", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JENNY_1_REWARD": { "label": "Route 124 - Swimmer Jenny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JEROME_REWARD": { "label": "Route 108 - Swimmer Jerome", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JERRY_1_REWARD": { "label": "Route 116 - School Kid Jerry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JESSICA_1_REWARD": { "label": "Route 121 - Beauty Jessica", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOCELYN_REWARD": { "label": "Dewford Gym - Battle Girl Jocelyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JODY_REWARD": { "label": "Petalburg Gym - Cooltrainer Jody", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOEY_REWARD": { "label": "Route 116 - Youngster Joey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOHANNA_REWARD": { "label": "Route 109 - Beauty Johanna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOHN_AND_JAY_1_REWARD": { "label": "Meteor Falls 1F - Old Couple John and Jay", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOHNSON_REWARD": { "label": "Route 116 - Youngster Johnson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JONAH_REWARD": { "label": "Route 127 - Fisherman Jonah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JONAS_REWARD": { "label": "Route 123 - Ninja Boy Jonas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JONATHAN_REWARD": { "label": "Route 132 - Cooltrainer Jonathan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSEPH_REWARD": { "label": "Route 110 - Guitarist Joseph", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSE_REWARD": { "label": "Route 116 - Bug Catcher Jose", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSH_REWARD": { "label": "Rustboro Gym - Youngster Josh", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSUE_REWARD": { "label": "Route 105 - Bird Keeper Josue", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JUAN_1_REWARD": { "label": "Sootopolis Gym - Leader Juan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JULIE_REWARD": { "label": "Victory Road B2F - Cooltrainer Julie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JULIO_REWARD": { "label": "Jagged Pass - Triathlete Julio", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAI_REWARD": { "label": "Route 114 - Fisherman Kai", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KALEB_REWARD": { "label": "Route 110 - Pokefan Kaleb", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KARA_REWARD": { "label": "Route 131 - Swimmer Kara", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAREN_1_REWARD": { "label": "Route 116 - School Kid Karen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATE_AND_JOY_REWARD": { "label": "Route 121 - Sr. and Jr. Kate and Joy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATELYNN_REWARD": { "label": "Victory Road 1F - Cooltrainer Katelynn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATELYN_1_REWARD": { "label": "Route 128 - Triathlete Katelyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATHLEEN_REWARD": { "label": "Mossdeep Gym - Hex Maniac Kathleen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATIE_REWARD": { "label": "Route 130 - Swimmer Katie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAYLA_REWARD": { "label": "Mt Pyre 3F - Psychic Kayla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAYLEY_REWARD": { "label": "Route 123 - Parasol Lady Kayley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KEEGAN_REWARD": { "label": "Lavaridge Gym - Kindler Keegan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KEIGO_REWARD": { "label": "Route 120 - Ninja Boy Keigo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KELVIN_REWARD": { "label": "Route 134 - Sailor Kelvin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KENT_REWARD": { "label": "Route 119 - Bug Catcher Kent", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KEVIN_REWARD": { "label": "Route 131 - Swimmer Kevin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIM_AND_IRIS_REWARD": { "label": "Route 125 - Sr. and Jr. Kim and Iris", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KINDRA_REWARD": { "label": "Route 123 - Hex Maniac Kindra", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIRA_AND_DAN_1_REWARD": { "label": "Abandoned Ship 1F - Young Couple Kira and Dan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIRK_REWARD": { "label": "Mauville Gym - Guitarist Kirk", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIYO_REWARD": { "label": "Route 132 - Black Belt Kiyo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KOICHI_REWARD": { "label": "Route 115 - Black Belt Koichi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KOJI_1_REWARD": { "label": "Route 127 - Black Belt Koji", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KYLA_REWARD": { "label": "Route 106 - Swimmer Kyla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KYRA_REWARD": { "label": "Route 115 - Triathlete Kyra", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAO_1_REWARD": { "label": "Route 113 - Ninja Boy Lao", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LARRY_REWARD": { "label": "Route 112 - Camper Larry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAURA_REWARD": { "label": "Dewford Gym - Battle Girl Laura", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAUREL_REWARD": { "label": "Route 134 - Swimmer Laurel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAWRENCE_REWARD": { "label": "Route 113 - Camper Lawrence", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEA_AND_JED_REWARD": { "label": "SS Tidal - Young Couple Lea and Jed", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEAH_REWARD": { "label": "Mt Pyre 2F - Hex Maniac Leah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LENNY_REWARD": { "label": "Route 114 - Hiker Lenny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEONARDO_REWARD": { "label": "Route 126 - Swimmer Leonardo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEONARD_REWARD": { "label": "SS Tidal - Sailor Leonard", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEONEL_REWARD": { "label": "Route 120 - Cooltrainer Leonel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LILA_AND_ROY_1_REWARD": { "label": "Route 124 - Sis and Bro Lila and Roy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LILITH_REWARD": { "label": "Dewford Gym - Battle Girl Lilith", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LINDA_REWARD": { "label": "Route 133 - Swimmer Linda", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LISA_AND_RAY_REWARD": { "label": "Route 107 - Sis and Bro Lisa and Ray", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LOLA_1_REWARD": { "label": "Route 109 - Tuber Lola", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LORENZO_REWARD": { "label": "Route 120 - Pokemon Ranger Lorenzo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LUCAS_1_REWARD": { "label": "Route 114 - Hiker Lucas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LUIS_REWARD": { "label": "Route 105 - Swimmer Luis", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LUNG_REWARD": { "label": "Route 113 - Ninja Boy Lung", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LYDIA_1_REWARD": { "label": "Route 117 - Pokemon Breeder Lydia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LYLE_REWARD": { "label": "Petalburg Woods - Bug Catcher Lyle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MACEY_REWARD": { "label": "Mossdeep Gym - Psychic Macey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MADELINE_1_REWARD": { "label": "Route 113 - Parasol Lady Madeline", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAKAYLA_REWARD": { "label": "Route 132 - Expert Makayla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARCEL_REWARD": { "label": "Route 121 - Cooltrainer Marcel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARCOS_REWARD": { "label": "Route 103 - Guitarist Marcos", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARC_REWARD": { "label": "Rustboro Gym - Hiker Marc", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARIA_1_REWARD": { "label": "Route 117 - Triathlete Maria", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARK_REWARD": { "label": "Mt Pyre 2F - Pokemaniac Mark", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARLENE_REWARD": { "label": "Route 115 - Psychic Marlene", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARLEY_REWARD": { "label": "Route 134 - Cooltrainer Marley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARY_REWARD": { "label": "Petalburg Gym - Cooltrainer Mary", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MATTHEW_REWARD": { "label": "Route 108 - Swimmer Matthew", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MATT_REWARD": { "label": "Aqua Hideout B2F - Aqua Admin Matt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAURA_REWARD": { "label": "Mossdeep Gym - Psychic Maura", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAXIE_MAGMA_HIDEOUT_REWARD": { "label": "Magma Hideout 4F - Magma Leader Maxie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAXIE_MT_CHIMNEY_REWARD": { "label": "Mt Chimney - Magma Leader Maxie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MEL_AND_PAUL_REWARD": { "label": "Route 109 - Young Couple Mel and Paul", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MELINA_REWARD": { "label": "Route 117 - Triathlete Melina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MELISSA_REWARD": { "label": "Mt Chimney - Beauty Melissa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MICAH_REWARD": { "label": "SS Tidal - Gentleman Micah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MICHELLE_REWARD": { "label": "Victory Road B1F - Cooltrainer Michelle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MIGUEL_1_REWARD": { "label": "Route 103 - Pokefan Miguel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MIKE_2_REWARD": { "label": "Rusturf Tunnel - Hiker Mike", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MISSY_REWARD": { "label": "Route 108 - Swimmer Missy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MITCHELL_REWARD": { "label": "Victory Road B1F - Cooltrainer Mitchell", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MIU_AND_YUKI_REWARD": { "label": "Route 123 - Twins Miu and Yuki", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MOLLIE_REWARD": { "label": "Route 133 - Expert Mollie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MYLES_REWARD": { "label": "Route 121 - Pokemon Breeder Myles", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NANCY_REWARD": { "label": "Route 114 - Picnicker Nancy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NAOMI_REWARD": { "label": "SS Tidal - Lady Naomi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NATE_REWARD": { "label": "Mossdeep Gym - Gentleman Nate", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NED_REWARD": { "label": "Route 106 - Fisherman Ned", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NICHOLAS_REWARD": { "label": "Mossdeep Gym - Psychic Nicholas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NICOLAS_1_REWARD": { "label": "Meteor Falls 1F - Dragon Tamer Nicolas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NIKKI_REWARD": { "label": "Route 126 - Swimmer Nikki", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NOB_1_REWARD": { "label": "Route 115 - Black Belt Nob", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NOLAN_REWARD": { "label": "Route 114 - Fisherman Nolan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NOLEN_REWARD": { "label": "Route 125 - Swimmer Nolen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NORMAN_1_REWARD": { "label": "Petalburg Gym - Leader Norman", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_OLIVIA_REWARD": { "label": "Sootopolis Gym - Beauty Olivia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_OWEN_REWARD": { "label": "Victory Road B2F - Cooltrainer Owen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PABLO_1_REWARD": { "label": "Route 126 - Triathlete Pablo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PARKER_REWARD": { "label": "Petalburg Gym - Cooltrainer Parker", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PAT_REWARD": { "label": "Route 121 - Pokemon Breeder Pat", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PAXTON_REWARD": { "label": "Route 132 - Expert Paxton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PERRY_REWARD": { "label": "Route 118 - Bird Keeper Perry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PETE_REWARD": { "label": "Route 103 - Swimmer Pete", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PHILLIP_REWARD": { "label": "SS Tidal - Sailor Phillip", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PHIL_REWARD": { "label": "Route 119 - Bird Keeper Phil", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PHOEBE_REWARD": { "label": "Ever Grande City - Elite Four Phoebe", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PRESLEY_REWARD": { "label": "Route 125 - Bird Keeper Presley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PRESTON_REWARD": { "label": "Mossdeep Gym - Psychic Preston", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_QUINCY_REWARD": { "label": "Victory Road 1F - Cooltrainer Quincy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RACHEL_REWARD": { "label": "Route 119 - Parasol Lady Rachel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RANDALL_REWARD": { "label": "Petalburg Gym - Cooltrainer Randall", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_REED_REWARD": { "label": "Route 129 - Swimmer Reed", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RELI_AND_IAN_REWARD": { "label": "Route 131 - Sis and Bro Reli and Ian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_REYNA_REWARD": { "label": "Route 134 - Battle Girl Reyna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RHETT_REWARD": { "label": "Route 103 - Black Belt Rhett", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RICHARD_REWARD": { "label": "Route 131 - Swimmer Richard", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RICKY_1_REWARD": { "label": "Route 109 - Tuber Ricky", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RICK_REWARD": { "label": "Route 102 - Bug Catcher Rick", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RILEY_REWARD": { "label": "Route 120 - Ninja Boy Riley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROBERT_1_REWARD": { "label": "Route 120 - Bird Keeper Robert", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RODNEY_REWARD": { "label": "Route 130 - Swimmer Rodney", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROGER_REWARD": { "label": "Route 127 - Fisherman Roger", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROLAND_REWARD": { "label": "Route 124 - Swimmer Roland", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RONALD_REWARD": { "label": "Route 132 - Fisherman Ronald", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROSE_1_REWARD": { "label": "Route 118 - Aroma Lady Rose", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROXANNE_1_REWARD": { "label": "Rustboro Gym - Leader Roxanne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RUBEN_REWARD": { "label": "Route 128 - Cooltrainer Ruben", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SAMANTHA_REWARD": { "label": "Mossdeep Gym - Psychic Samantha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SAMUEL_REWARD": { "label": "Victory Road B1F - Cooltrainer Samuel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SANTIAGO_REWARD": { "label": "Route 130 - Swimmer Santiago", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SARAH_REWARD": { "label": "Route 116 - Lady Sarah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SAWYER_1_REWARD": { "label": "Mt Chimney - Hiker Sawyer", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHANE_REWARD": { "label": "Route 114 - Camper Shane", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHANNON_REWARD": { "label": "Victory Road B1F - Cooltrainer Shannon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHARON_REWARD": { "label": "Route 125 - Swimmer Sharon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHAWN_REWARD": { "label": "Mauville Gym - Guitarist Shawn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHAYLA_REWARD": { "label": "Route 112 - Aroma Lady Shayla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHEILA_REWARD": { "label": "Mt Chimney - Beauty Sheila", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHELBY_1_REWARD": { "label": "Mt Chimney - Expert Shelby", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHELLY_SEAFLOOR_CAVERN_REWARD": { "label": "Seafloor Cavern Room 3 - Aqua Admin Shelly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHELLY_WEATHER_INSTITUTE_REWARD": { "label": "Weather Institute 2F - Aqua Admin Shelly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHIRLEY_REWARD": { "label": "Mt Chimney - Beauty Shirley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SIDNEY_REWARD": { "label": "Ever Grande City - Elite Four Sidney", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SIENNA_REWARD": { "label": "Route 126 - Swimmer Sienna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SIMON_REWARD": { "label": "Route 109 - Tuber Simon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SOPHIE_REWARD": { "label": "Route 113 - Picnicker Sophie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SPENCER_REWARD": { "label": "Route 124 - Swimmer Spencer", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_STAN_REWARD": { "label": "Route 125 - Swimmer Stan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_STEVEN_REWARD": { "label": "Meteor Falls 1F - Rival Steven", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_STEVE_1_REWARD": { "label": "Route 114 - Pokemaniac Steve", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SUSIE_REWARD": { "label": "Route 131 - Swimmer Susie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SYLVIA_REWARD": { "label": "Mossdeep Gym - Hex Maniac Sylvia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TABITHA_MAGMA_HIDEOUT_REWARD": { "label": "Magma Hideout 4F - Magma Admin Tabitha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TABITHA_MT_CHIMNEY_REWARD": { "label": "Mt Chimney - Magma Admin Tabitha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAKAO_REWARD": { "label": "Dewford Gym - Black Belt Takao", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAKASHI_REWARD": { "label": "Route 119 - Ninja Boy Takashi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TALIA_REWARD": { "label": "Route 131 - Triathlete Talia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAMMY_REWARD": { "label": "Route 121 - Hex Maniac Tammy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TANYA_REWARD": { "label": "Route 125 - Swimmer Tanya", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TARA_REWARD": { "label": "Route 108 - Swimmer Tara", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TASHA_REWARD": { "label": "Mt Pyre 4F - Hex Maniac Tasha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TATE_AND_LIZA_1_REWARD": { "label": "Mossdeep Gym - Leader Tate and Liza", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAYLOR_REWARD": { "label": "Route 119 - Bug Maniac Taylor", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TYRA_AND_IVY_REWARD": { "label": "Route 114 - Sr. and Jr. Tyra and Ivy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_THALIA_1_REWARD": { "label": "Abandoned Ship 1F - Beauty Thalia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_THOMAS_REWARD": { "label": "SS Tidal - Gentleman Thomas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIANA_REWARD": { "label": "Route 102 - Lass Tiana", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIFFANY_REWARD": { "label": "Sootopolis Gym - Beauty Tiffany", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIMMY_REWARD": { "label": "Route 110 - Youngster Timmy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIMOTHY_1_REWARD": { "label": "Route 115 - Expert Timothy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TISHA_REWARD": { "label": "Route 129 - Swimmer Tisha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TOMMY_REWARD": { "label": "Rustboro Gym - Youngster Tommy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TONY_1_REWARD": { "label": "Route 107 - Swimmer Tony", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TORI_AND_TIA_REWARD": { "label": "Route 113 - Twins Tori and Tia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TRAVIS_REWARD": { "label": "Route 111 - Camper Travis", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TRENT_1_REWARD": { "label": "Route 112 - Hiker Trent", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TYRON_REWARD": { "label": "Route 111 - Camper Tyron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VALERIE_1_REWARD": { "label": "Mt Pyre 6F - Hex Maniac Valerie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VANESSA_REWARD": { "label": "Route 121 - Pokefan Vanessa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VICKY_REWARD": { "label": "Route 111 - Winstrate Vicky", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VICTORIA_REWARD": { "label": "Route 111 - Winstrate Victoria", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VICTOR_REWARD": { "label": "Route 111 - Winstrate Victor", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIOLET_REWARD": { "label": "Route 123 - Aroma Lady Violet", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIRGIL_REWARD": { "label": "Mossdeep Gym - Psychic Virgil", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VITO_REWARD": { "label": "Victory Road B2F - Cooltrainer Vito", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIVIAN_REWARD": { "label": "Mauville Gym - Battle Girl Vivian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIVI_REWARD": { "label": "Route 111 - Winstrate Vivi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WADE_REWARD": { "label": "Route 118 - Fisherman Wade", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALLACE_REWARD": { "label": "Ever Grande City - Champion Wallace", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALTER_1_REWARD": { "label": "Route 121 - Gentleman Walter", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALLY_MAUVILLE_REWARD": { "label": "Mauville City - Rival Wally", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALLY_VR_1_REWARD": { "label": "Victory Road 1F - Rival Wally", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WATTSON_1_REWARD": { "label": "Mauville Gym - Leader Wattson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WARREN_REWARD": { "label": "Route 133 - Cooltrainer Warren", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WAYNE_REWARD": { "label": "Route 128 - Fisherman Wayne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WENDY_REWARD": { "label": "Route 123 - Cooltrainer Wendy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WILLIAM_REWARD": { "label": "Mt Pyre 3F - Psychic William", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WILTON_1_REWARD": { "label": "Route 111 - Cooltrainer Wilton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WINONA_1_REWARD": { "label": "Fortree Gym - Leader Winona", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WINSTON_1_REWARD": { "label": "Route 104 - Rich Boy Winston", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WYATT_REWARD": { "label": "Route 113 - Pokemaniac Wyatt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_YASU_REWARD": { "label": "Route 119 - Ninja Boy Yasu", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ZANDER_REWARD": { "label": "Mt Pyre 2F - Black Belt Zander", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" } } diff --git a/worlds/pokemon_emerald/groups.py b/worlds/pokemon_emerald/groups.py new file mode 100644 index 000000000000..d358da18350f --- /dev/null +++ b/worlds/pokemon_emerald/groups.py @@ -0,0 +1,721 @@ +from typing import Dict, Set + +from .data import LocationCategory, data + + +# Item Groups +ITEM_GROUPS: Dict[str, Set[str]] = {} + +for item in data.items.values(): + for tag in item.tags: + if tag not in ITEM_GROUPS: + ITEM_GROUPS[tag] = set() + ITEM_GROUPS[tag].add(item.label) + +# Location Groups +_LOCATION_GROUP_MAPS = { + "Abandoned Ship": { + "MAP_ABANDONED_SHIP_CAPTAINS_OFFICE", + "MAP_ABANDONED_SHIP_CORRIDORS_1F", + "MAP_ABANDONED_SHIP_CORRIDORS_B1F", + "MAP_ABANDONED_SHIP_DECK", + "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS", + "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS", + "MAP_ABANDONED_SHIP_ROOMS2_1F", + "MAP_ABANDONED_SHIP_ROOMS2_B1F", + "MAP_ABANDONED_SHIP_ROOMS_1F", + "MAP_ABANDONED_SHIP_ROOMS_B1F", + "MAP_ABANDONED_SHIP_ROOM_B1F", + "MAP_ABANDONED_SHIP_UNDERWATER1", + "MAP_ABANDONED_SHIP_UNDERWATER2", + }, + "Aqua Hideout": { + "MAP_AQUA_HIDEOUT_1F", + "MAP_AQUA_HIDEOUT_B1F", + "MAP_AQUA_HIDEOUT_B2F", + }, + "Battle Frontier": { + "MAP_ARTISAN_CAVE_1F", + "MAP_ARTISAN_CAVE_B1F", + "MAP_BATTLE_FRONTIER_BATTLE_ARENA_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_ARENA_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_PRE_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_FINAL", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_NORMAL", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_WILD_MONS", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_THREE_PATH_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_FLOOR", + "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_TOP", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_ELEVATOR", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_PARTNER_ROOM", + "MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER", + "MAP_BATTLE_FRONTIER_LOUNGE1", + "MAP_BATTLE_FRONTIER_LOUNGE2", + "MAP_BATTLE_FRONTIER_LOUNGE3", + "MAP_BATTLE_FRONTIER_LOUNGE4", + "MAP_BATTLE_FRONTIER_LOUNGE5", + "MAP_BATTLE_FRONTIER_LOUNGE6", + "MAP_BATTLE_FRONTIER_LOUNGE7", + "MAP_BATTLE_FRONTIER_LOUNGE8", + "MAP_BATTLE_FRONTIER_LOUNGE9", + "MAP_BATTLE_FRONTIER_MART", + "MAP_BATTLE_FRONTIER_OUTSIDE_EAST", + "MAP_BATTLE_FRONTIER_OUTSIDE_WEST", + "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F", + "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F", + "MAP_BATTLE_FRONTIER_RANKING_HALL", + "MAP_BATTLE_FRONTIER_RECEPTION_GATE", + "MAP_BATTLE_FRONTIER_SCOTTS_HOUSE", + "MAP_BATTLE_PYRAMID_SQUARE01", + "MAP_BATTLE_PYRAMID_SQUARE02", + "MAP_BATTLE_PYRAMID_SQUARE03", + "MAP_BATTLE_PYRAMID_SQUARE04", + "MAP_BATTLE_PYRAMID_SQUARE05", + "MAP_BATTLE_PYRAMID_SQUARE06", + "MAP_BATTLE_PYRAMID_SQUARE07", + "MAP_BATTLE_PYRAMID_SQUARE08", + "MAP_BATTLE_PYRAMID_SQUARE09", + "MAP_BATTLE_PYRAMID_SQUARE10", + "MAP_BATTLE_PYRAMID_SQUARE11", + "MAP_BATTLE_PYRAMID_SQUARE12", + "MAP_BATTLE_PYRAMID_SQUARE13", + "MAP_BATTLE_PYRAMID_SQUARE14", + "MAP_BATTLE_PYRAMID_SQUARE15", + "MAP_BATTLE_PYRAMID_SQUARE16", + }, + "Birth Island": { + "MAP_BIRTH_ISLAND_EXTERIOR", + "MAP_BIRTH_ISLAND_HARBOR", + }, + "Contest Hall": { + "MAP_CONTEST_HALL", + "MAP_CONTEST_HALL_BEAUTY", + "MAP_CONTEST_HALL_COOL", + "MAP_CONTEST_HALL_CUTE", + "MAP_CONTEST_HALL_SMART", + "MAP_CONTEST_HALL_TOUGH", + }, + "Dewford Town": { + "MAP_DEWFORD_TOWN", + "MAP_DEWFORD_TOWN_GYM", + "MAP_DEWFORD_TOWN_HALL", + "MAP_DEWFORD_TOWN_HOUSE1", + "MAP_DEWFORD_TOWN_HOUSE2", + "MAP_DEWFORD_TOWN_POKEMON_CENTER_1F", + "MAP_DEWFORD_TOWN_POKEMON_CENTER_2F", + }, + "Ever Grande City": { + "MAP_EVER_GRANDE_CITY", + "MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM", + "MAP_EVER_GRANDE_CITY_DRAKES_ROOM", + "MAP_EVER_GRANDE_CITY_GLACIAS_ROOM", + "MAP_EVER_GRANDE_CITY_HALL1", + "MAP_EVER_GRANDE_CITY_HALL2", + "MAP_EVER_GRANDE_CITY_HALL3", + "MAP_EVER_GRANDE_CITY_HALL4", + "MAP_EVER_GRANDE_CITY_HALL5", + "MAP_EVER_GRANDE_CITY_HALL_OF_FAME", + "MAP_EVER_GRANDE_CITY_PHOEBES_ROOM", + "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F", + "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F", + "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F", + "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F", + "MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM", + }, + "Fallarbor Town": { + "MAP_FALLARBOR_TOWN", + "MAP_FALLARBOR_TOWN_BATTLE_TENT_BATTLE_ROOM", + "MAP_FALLARBOR_TOWN_BATTLE_TENT_CORRIDOR", + "MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY", + "MAP_FALLARBOR_TOWN_COZMOS_HOUSE", + "MAP_FALLARBOR_TOWN_MART", + "MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE", + "MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F", + "MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F", + }, + "Faraway Island": { + "MAP_FARAWAY_ISLAND_ENTRANCE", + "MAP_FARAWAY_ISLAND_INTERIOR", + }, + "Fiery Path": {"MAP_FIERY_PATH"}, + "Fortree City": { + "MAP_FORTREE_CITY", + "MAP_FORTREE_CITY_DECORATION_SHOP", + "MAP_FORTREE_CITY_GYM", + "MAP_FORTREE_CITY_HOUSE1", + "MAP_FORTREE_CITY_HOUSE2", + "MAP_FORTREE_CITY_HOUSE3", + "MAP_FORTREE_CITY_HOUSE4", + "MAP_FORTREE_CITY_HOUSE5", + "MAP_FORTREE_CITY_MART", + "MAP_FORTREE_CITY_POKEMON_CENTER_1F", + "MAP_FORTREE_CITY_POKEMON_CENTER_2F", + }, + "Granite Cave": { + "MAP_GRANITE_CAVE_1F", + "MAP_GRANITE_CAVE_B1F", + "MAP_GRANITE_CAVE_B2F", + "MAP_GRANITE_CAVE_STEVENS_ROOM", + }, + "Jagged Pass": {"MAP_JAGGED_PASS"}, + "Lavaridge Town": { + "MAP_LAVARIDGE_TOWN", + "MAP_LAVARIDGE_TOWN_GYM_1F", + "MAP_LAVARIDGE_TOWN_GYM_B1F", + "MAP_LAVARIDGE_TOWN_HERB_SHOP", + "MAP_LAVARIDGE_TOWN_HOUSE", + "MAP_LAVARIDGE_TOWN_MART", + "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F", + "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F", + }, + "Lilycove City": { + "MAP_LILYCOVE_CITY", + "MAP_LILYCOVE_CITY_CONTEST_HALL", + "MAP_LILYCOVE_CITY_CONTEST_LOBBY", + "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F", + "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP", + "MAP_LILYCOVE_CITY_HARBOR", + "MAP_LILYCOVE_CITY_HOUSE1", + "MAP_LILYCOVE_CITY_HOUSE2", + "MAP_LILYCOVE_CITY_HOUSE3", + "MAP_LILYCOVE_CITY_HOUSE4", + "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F", + "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F", + "MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE", + "MAP_LILYCOVE_CITY_POKEMON_CENTER_1F", + "MAP_LILYCOVE_CITY_POKEMON_CENTER_2F", + "MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB", + }, + "Littleroot Town": { + "MAP_INSIDE_OF_TRUCK", + "MAP_LITTLEROOT_TOWN", + "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F", + "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F", + "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F", + "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F", + "MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB", + }, + "Magma Hideout": { + "MAP_MAGMA_HIDEOUT_1F", + "MAP_MAGMA_HIDEOUT_2F_1R", + "MAP_MAGMA_HIDEOUT_2F_2R", + "MAP_MAGMA_HIDEOUT_2F_3R", + "MAP_MAGMA_HIDEOUT_3F_1R", + "MAP_MAGMA_HIDEOUT_3F_2R", + "MAP_MAGMA_HIDEOUT_3F_3R", + "MAP_MAGMA_HIDEOUT_4F", + }, + "Marine Cave": { + "MAP_MARINE_CAVE_END", + "MAP_MARINE_CAVE_ENTRANCE", + "MAP_UNDERWATER_MARINE_CAVE", + }, + "Mauville City": { + "MAP_MAUVILLE_CITY", + "MAP_MAUVILLE_CITY_BIKE_SHOP", + "MAP_MAUVILLE_CITY_GAME_CORNER", + "MAP_MAUVILLE_CITY_GYM", + "MAP_MAUVILLE_CITY_HOUSE1", + "MAP_MAUVILLE_CITY_HOUSE2", + "MAP_MAUVILLE_CITY_MART", + "MAP_MAUVILLE_CITY_POKEMON_CENTER_1F", + "MAP_MAUVILLE_CITY_POKEMON_CENTER_2F", + }, + "Meteor Falls": { + "MAP_METEOR_FALLS_1F_1R", + "MAP_METEOR_FALLS_1F_2R", + "MAP_METEOR_FALLS_B1F_1R", + "MAP_METEOR_FALLS_B1F_2R", + "MAP_METEOR_FALLS_STEVENS_CAVE", + }, + "Mirage Tower": { + "MAP_MIRAGE_TOWER_1F", + "MAP_MIRAGE_TOWER_2F", + "MAP_MIRAGE_TOWER_3F", + "MAP_MIRAGE_TOWER_4F", + }, + "Mossdeep City": { + "MAP_MOSSDEEP_CITY", + "MAP_MOSSDEEP_CITY_GAME_CORNER_1F", + "MAP_MOSSDEEP_CITY_GAME_CORNER_B1F", + "MAP_MOSSDEEP_CITY_GYM", + "MAP_MOSSDEEP_CITY_HOUSE1", + "MAP_MOSSDEEP_CITY_HOUSE2", + "MAP_MOSSDEEP_CITY_HOUSE3", + "MAP_MOSSDEEP_CITY_HOUSE4", + "MAP_MOSSDEEP_CITY_MART", + "MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F", + "MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F", + "MAP_MOSSDEEP_CITY_SPACE_CENTER_1F", + "MAP_MOSSDEEP_CITY_SPACE_CENTER_2F", + "MAP_MOSSDEEP_CITY_STEVENS_HOUSE", + }, + "Mt. Chimney": { + "MAP_MT_CHIMNEY", + "MAP_MT_CHIMNEY_CABLE_CAR_STATION", + }, + "Mt. Pyre": { + "MAP_MT_PYRE_1F", + "MAP_MT_PYRE_2F", + "MAP_MT_PYRE_3F", + "MAP_MT_PYRE_4F", + "MAP_MT_PYRE_5F", + "MAP_MT_PYRE_6F", + "MAP_MT_PYRE_EXTERIOR", + "MAP_MT_PYRE_SUMMIT", + }, + "Navel Rock": { + "MAP_NAVEL_ROCK_B1F", + "MAP_NAVEL_ROCK_BOTTOM", + "MAP_NAVEL_ROCK_DOWN01", + "MAP_NAVEL_ROCK_DOWN02", + "MAP_NAVEL_ROCK_DOWN03", + "MAP_NAVEL_ROCK_DOWN04", + "MAP_NAVEL_ROCK_DOWN05", + "MAP_NAVEL_ROCK_DOWN06", + "MAP_NAVEL_ROCK_DOWN07", + "MAP_NAVEL_ROCK_DOWN08", + "MAP_NAVEL_ROCK_DOWN09", + "MAP_NAVEL_ROCK_DOWN10", + "MAP_NAVEL_ROCK_DOWN11", + "MAP_NAVEL_ROCK_ENTRANCE", + "MAP_NAVEL_ROCK_EXTERIOR", + "MAP_NAVEL_ROCK_FORK", + "MAP_NAVEL_ROCK_HARBOR", + "MAP_NAVEL_ROCK_TOP", + "MAP_NAVEL_ROCK_UP1", + "MAP_NAVEL_ROCK_UP2", + "MAP_NAVEL_ROCK_UP3", + "MAP_NAVEL_ROCK_UP4", + }, + "New Mauville": { + "MAP_NEW_MAUVILLE_ENTRANCE", + "MAP_NEW_MAUVILLE_INSIDE", + }, + "Oldale Town": { + "MAP_OLDALE_TOWN", + "MAP_OLDALE_TOWN_HOUSE1", + "MAP_OLDALE_TOWN_HOUSE2", + "MAP_OLDALE_TOWN_MART", + "MAP_OLDALE_TOWN_POKEMON_CENTER_1F", + "MAP_OLDALE_TOWN_POKEMON_CENTER_2F", + }, + "Pacifidlog Town": { + "MAP_PACIFIDLOG_TOWN", + "MAP_PACIFIDLOG_TOWN_HOUSE1", + "MAP_PACIFIDLOG_TOWN_HOUSE2", + "MAP_PACIFIDLOG_TOWN_HOUSE3", + "MAP_PACIFIDLOG_TOWN_HOUSE4", + "MAP_PACIFIDLOG_TOWN_HOUSE5", + "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F", + "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F", + }, + "Petalburg City": { + "MAP_PETALBURG_CITY", + "MAP_PETALBURG_CITY_GYM", + "MAP_PETALBURG_CITY_HOUSE1", + "MAP_PETALBURG_CITY_HOUSE2", + "MAP_PETALBURG_CITY_MART", + "MAP_PETALBURG_CITY_POKEMON_CENTER_1F", + "MAP_PETALBURG_CITY_POKEMON_CENTER_2F", + "MAP_PETALBURG_CITY_WALLYS_HOUSE", + }, + "Petalburg Woods": {"MAP_PETALBURG_WOODS"}, + "Route 101": {"MAP_ROUTE101"}, + "Route 102": {"MAP_ROUTE102"}, + "Route 103": {"MAP_ROUTE103"}, + "Route 104": { + "MAP_ROUTE104", + "MAP_ROUTE104_MR_BRINEYS_HOUSE", + "MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP", + }, + "Route 105": { + "MAP_ISLAND_CAVE", + "MAP_ROUTE105", + "MAP_UNDERWATER_ROUTE105", + }, + "Route 106": {"MAP_ROUTE106"}, + "Route 107": {"MAP_ROUTE107"}, + "Route 108": {"MAP_ROUTE108"}, + "Route 109": { + "MAP_ROUTE109", + "MAP_ROUTE109_SEASHORE_HOUSE", + }, + "Route 110": { + "MAP_ROUTE110", + "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE", + "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE", + }, + "Trick House": { + "MAP_ROUTE110_TRICK_HOUSE_CORRIDOR", + "MAP_ROUTE110_TRICK_HOUSE_END", + "MAP_ROUTE110_TRICK_HOUSE_ENTRANCE", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE1", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8", + }, + "Route 111": { + "MAP_DESERT_RUINS", + "MAP_ROUTE111", + "MAP_ROUTE111_OLD_LADYS_REST_STOP", + "MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE", + }, + "Route 112": { + "MAP_ROUTE112", + "MAP_ROUTE112_CABLE_CAR_STATION", + }, + "Route 113": { + "MAP_ROUTE113", + "MAP_ROUTE113_GLASS_WORKSHOP", + }, + "Route 114": { + "MAP_DESERT_UNDERPASS", + "MAP_ROUTE114", + "MAP_ROUTE114_FOSSIL_MANIACS_HOUSE", + "MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL", + "MAP_ROUTE114_LANETTES_HOUSE", + }, + "Route 115": {"MAP_ROUTE115"}, + "Route 116": { + "MAP_ROUTE116", + "MAP_ROUTE116_TUNNELERS_REST_HOUSE", + }, + "Route 117": { + "MAP_ROUTE117", + "MAP_ROUTE117_POKEMON_DAY_CARE", + }, + "Route 118": {"MAP_ROUTE118"}, + "Route 119": { + "MAP_ROUTE119", + "MAP_ROUTE119_HOUSE", + "MAP_ROUTE119_WEATHER_INSTITUTE_1F", + "MAP_ROUTE119_WEATHER_INSTITUTE_2F", + }, + "Route 120": { + "MAP_ANCIENT_TOMB", + "MAP_ROUTE120", + "MAP_SCORCHED_SLAB", + }, + "Route 121": { + "MAP_ROUTE121", + }, + "Route 122": {"MAP_ROUTE122"}, + "Route 123": { + "MAP_ROUTE123", + "MAP_ROUTE123_BERRY_MASTERS_HOUSE", + }, + "Route 124": { + "MAP_ROUTE124", + "MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE", + "MAP_UNDERWATER_ROUTE124", + }, + "Route 125": { + "MAP_ROUTE125", + "MAP_UNDERWATER_ROUTE125", + }, + "Route 126": { + "MAP_ROUTE126", + "MAP_UNDERWATER_ROUTE126", + }, + "Route 127": { + "MAP_ROUTE127", + "MAP_UNDERWATER_ROUTE127", + }, + "Route 128": { + "MAP_ROUTE128", + "MAP_UNDERWATER_ROUTE128", + }, + "Route 129": { + "MAP_ROUTE129", + "MAP_UNDERWATER_ROUTE129", + }, + "Route 130": {"MAP_ROUTE130"}, + "Route 131": {"MAP_ROUTE131"}, + "Route 132": {"MAP_ROUTE132"}, + "Route 133": {"MAP_ROUTE133"}, + "Route 134": { + "MAP_ROUTE134", + "MAP_UNDERWATER_ROUTE134", + "MAP_SEALED_CHAMBER_INNER_ROOM", + "MAP_SEALED_CHAMBER_OUTER_ROOM", + "MAP_UNDERWATER_SEALED_CHAMBER", + }, + "Rustboro City": { + "MAP_RUSTBORO_CITY", + "MAP_RUSTBORO_CITY_CUTTERS_HOUSE", + "MAP_RUSTBORO_CITY_DEVON_CORP_1F", + "MAP_RUSTBORO_CITY_DEVON_CORP_2F", + "MAP_RUSTBORO_CITY_DEVON_CORP_3F", + "MAP_RUSTBORO_CITY_FLAT1_1F", + "MAP_RUSTBORO_CITY_FLAT1_2F", + "MAP_RUSTBORO_CITY_FLAT2_1F", + "MAP_RUSTBORO_CITY_FLAT2_2F", + "MAP_RUSTBORO_CITY_FLAT2_3F", + "MAP_RUSTBORO_CITY_GYM", + "MAP_RUSTBORO_CITY_HOUSE1", + "MAP_RUSTBORO_CITY_HOUSE2", + "MAP_RUSTBORO_CITY_HOUSE3", + "MAP_RUSTBORO_CITY_MART", + "MAP_RUSTBORO_CITY_POKEMON_CENTER_1F", + "MAP_RUSTBORO_CITY_POKEMON_CENTER_2F", + "MAP_RUSTBORO_CITY_POKEMON_SCHOOL", + }, + "Rusturf Tunnel": {"MAP_RUSTURF_TUNNEL"}, + "Safari Zone": { + "MAP_ROUTE121_SAFARI_ZONE_ENTRANCE", + "MAP_SAFARI_ZONE_NORTH", + "MAP_SAFARI_ZONE_NORTHEAST", + "MAP_SAFARI_ZONE_NORTHWEST", + "MAP_SAFARI_ZONE_REST_HOUSE", + "MAP_SAFARI_ZONE_SOUTH", + "MAP_SAFARI_ZONE_SOUTHEAST", + "MAP_SAFARI_ZONE_SOUTHWEST", + }, + "Seafloor Cavern": { + "MAP_SEAFLOOR_CAVERN_ENTRANCE", + "MAP_SEAFLOOR_CAVERN_ROOM1", + "MAP_SEAFLOOR_CAVERN_ROOM2", + "MAP_SEAFLOOR_CAVERN_ROOM3", + "MAP_SEAFLOOR_CAVERN_ROOM4", + "MAP_SEAFLOOR_CAVERN_ROOM5", + "MAP_SEAFLOOR_CAVERN_ROOM6", + "MAP_SEAFLOOR_CAVERN_ROOM7", + "MAP_SEAFLOOR_CAVERN_ROOM8", + "MAP_SEAFLOOR_CAVERN_ROOM9", + "MAP_UNDERWATER_SEAFLOOR_CAVERN", + }, + "Shoal Cave": { + "MAP_SHOAL_CAVE_HIGH_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_HIGH_TIDE_INNER_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM", + }, + "Sky Pillar": { + "MAP_SKY_PILLAR_1F", + "MAP_SKY_PILLAR_2F", + "MAP_SKY_PILLAR_3F", + "MAP_SKY_PILLAR_4F", + "MAP_SKY_PILLAR_5F", + "MAP_SKY_PILLAR_ENTRANCE", + "MAP_SKY_PILLAR_OUTSIDE", + "MAP_SKY_PILLAR_TOP", + }, + "Slateport City": { + "MAP_SLATEPORT_CITY", + "MAP_SLATEPORT_CITY_BATTLE_TENT_BATTLE_ROOM", + "MAP_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR", + "MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY", + "MAP_SLATEPORT_CITY_HARBOR", + "MAP_SLATEPORT_CITY_HOUSE", + "MAP_SLATEPORT_CITY_MART", + "MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE", + "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F", + "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F", + "MAP_SLATEPORT_CITY_POKEMON_CENTER_1F", + "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F", + "MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB", + "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F", + "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F", + }, + "Sootopolis City": { + "MAP_CAVE_OF_ORIGIN_1F", + "MAP_CAVE_OF_ORIGIN_B1F", + "MAP_CAVE_OF_ORIGIN_ENTRANCE", + "MAP_SOOTOPOLIS_CITY", + "MAP_SOOTOPOLIS_CITY_GYM_1F", + "MAP_SOOTOPOLIS_CITY_GYM_B1F", + "MAP_SOOTOPOLIS_CITY_HOUSE1", + "MAP_SOOTOPOLIS_CITY_HOUSE2", + "MAP_SOOTOPOLIS_CITY_HOUSE3", + "MAP_SOOTOPOLIS_CITY_HOUSE4", + "MAP_SOOTOPOLIS_CITY_HOUSE5", + "MAP_SOOTOPOLIS_CITY_HOUSE6", + "MAP_SOOTOPOLIS_CITY_HOUSE7", + "MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE", + "MAP_SOOTOPOLIS_CITY_MART", + "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F", + "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F", + "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F", + "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F", + "MAP_UNDERWATER_SOOTOPOLIS_CITY", + }, + "Southern Island": { + "MAP_SOUTHERN_ISLAND_EXTERIOR", + "MAP_SOUTHERN_ISLAND_INTERIOR", + }, + "S.S. Tidal": { + "MAP_SS_TIDAL_CORRIDOR", + "MAP_SS_TIDAL_LOWER_DECK", + "MAP_SS_TIDAL_ROOMS", + }, + "Terra Cave": { + "MAP_TERRA_CAVE_END", + "MAP_TERRA_CAVE_ENTRANCE", + }, + "Trainer Hill": { + "MAP_TRAINER_HILL_2F", + "MAP_TRAINER_HILL_3F", + "MAP_TRAINER_HILL_4F", + "MAP_TRAINER_HILL_ELEVATOR", + "MAP_TRAINER_HILL_ENTRANCE", + "MAP_TRAINER_HILL_ROOF", + }, + "Verdanturf Town": { + "MAP_VERDANTURF_TOWN", + "MAP_VERDANTURF_TOWN_BATTLE_TENT_BATTLE_ROOM", + "MAP_VERDANTURF_TOWN_BATTLE_TENT_CORRIDOR", + "MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY", + "MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE", + "MAP_VERDANTURF_TOWN_HOUSE", + "MAP_VERDANTURF_TOWN_MART", + "MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F", + "MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F", + "MAP_VERDANTURF_TOWN_WANDAS_HOUSE", + }, + "Victory Road": { + "MAP_VICTORY_ROAD_1F", + "MAP_VICTORY_ROAD_B1F", + "MAP_VICTORY_ROAD_B2F", + }, +} + +_LOCATION_CATEGORY_TO_GROUP_NAME = { + LocationCategory.BADGE: "Badges", + LocationCategory.HM: "HMs", + LocationCategory.KEY: "Key Items", + LocationCategory.ROD: "Fishing Rods", + LocationCategory.BIKE: "Bikes", + LocationCategory.TICKET: "Tickets", + LocationCategory.OVERWORLD_ITEM: "Overworld Items", + LocationCategory.HIDDEN_ITEM: "Hidden Items", + LocationCategory.GIFT: "NPC Gifts", + LocationCategory.BERRY_TREE: "Berry Trees", + LocationCategory.TRAINER: "Trainers", + LocationCategory.POKEDEX: "Pokedex", +} + +LOCATION_GROUPS: Dict[str, Set[str]] = {group_name: set() for group_name in _LOCATION_CATEGORY_TO_GROUP_NAME.values()} +for location in data.locations.values(): + # Category groups + LOCATION_GROUPS[_LOCATION_CATEGORY_TO_GROUP_NAME[location.category]].add(location.label) + + # Tag groups + for tag in location.tags: + if tag not in LOCATION_GROUPS: + LOCATION_GROUPS[tag] = set() + LOCATION_GROUPS[tag].add(location.label) + + # Geographic groups + if location.parent_region != "REGION_POKEDEX": + map_name = data.regions[location.parent_region].parent_map.name + for group, maps in _LOCATION_GROUP_MAPS.items(): + if map_name in maps: + if group not in LOCATION_GROUPS: + LOCATION_GROUPS[group] = set() + LOCATION_GROUPS[group].add(location.label) + break + +# Meta-groups +LOCATION_GROUPS["Cities"] = { + *LOCATION_GROUPS.get("Littleroot Town", set()), + *LOCATION_GROUPS.get("Oldale Town", set()), + *LOCATION_GROUPS.get("Petalburg City", set()), + *LOCATION_GROUPS.get("Rustboro City", set()), + *LOCATION_GROUPS.get("Dewford Town", set()), + *LOCATION_GROUPS.get("Slateport City", set()), + *LOCATION_GROUPS.get("Mauville City", set()), + *LOCATION_GROUPS.get("Verdanturf Town", set()), + *LOCATION_GROUPS.get("Fallarbor Town", set()), + *LOCATION_GROUPS.get("Lavaridge Town", set()), + *LOCATION_GROUPS.get("Fortree City", set()), + *LOCATION_GROUPS.get("Mossdeep City", set()), + *LOCATION_GROUPS.get("Sootopolis City", set()), + *LOCATION_GROUPS.get("Pacifidlog Town", set()), + *LOCATION_GROUPS.get("Ever Grande City", set()), +} + +LOCATION_GROUPS["Dungeons"] = { + *LOCATION_GROUPS.get("Petalburg Woods", set()), + *LOCATION_GROUPS.get("Rusturf Tunnel", set()), + *LOCATION_GROUPS.get("Granite Cave", set()), + *LOCATION_GROUPS.get("Fiery Path", set()), + *LOCATION_GROUPS.get("Meteor Falls", set()), + *LOCATION_GROUPS.get("Jagged Pass", set()), + *LOCATION_GROUPS.get("Mt. Chimney", set()), + *LOCATION_GROUPS.get("Abandoned Ship", set()), + *LOCATION_GROUPS.get("New Mauville", set()), + *LOCATION_GROUPS.get("Mt. Pyre", set()), + *LOCATION_GROUPS.get("Seafloor Cavern", set()), + *LOCATION_GROUPS.get("Sky Pillar", set()), + *LOCATION_GROUPS.get("Victory Road", set()), +} + +LOCATION_GROUPS["Routes"] = { + *LOCATION_GROUPS.get("Route 101", set()), + *LOCATION_GROUPS.get("Route 102", set()), + *LOCATION_GROUPS.get("Route 103", set()), + *LOCATION_GROUPS.get("Route 104", set()), + *LOCATION_GROUPS.get("Route 105", set()), + *LOCATION_GROUPS.get("Route 106", set()), + *LOCATION_GROUPS.get("Route 107", set()), + *LOCATION_GROUPS.get("Route 108", set()), + *LOCATION_GROUPS.get("Route 109", set()), + *LOCATION_GROUPS.get("Route 110", set()), + *LOCATION_GROUPS.get("Route 111", set()), + *LOCATION_GROUPS.get("Route 112", set()), + *LOCATION_GROUPS.get("Route 113", set()), + *LOCATION_GROUPS.get("Route 114", set()), + *LOCATION_GROUPS.get("Route 115", set()), + *LOCATION_GROUPS.get("Route 116", set()), + *LOCATION_GROUPS.get("Route 117", set()), + *LOCATION_GROUPS.get("Route 118", set()), + *LOCATION_GROUPS.get("Route 119", set()), + *LOCATION_GROUPS.get("Route 120", set()), + *LOCATION_GROUPS.get("Route 121", set()), + *LOCATION_GROUPS.get("Route 122", set()), + *LOCATION_GROUPS.get("Route 123", set()), + *LOCATION_GROUPS.get("Route 124", set()), + *LOCATION_GROUPS.get("Route 125", set()), + *LOCATION_GROUPS.get("Route 126", set()), + *LOCATION_GROUPS.get("Route 127", set()), + *LOCATION_GROUPS.get("Route 128", set()), + *LOCATION_GROUPS.get("Route 129", set()), + *LOCATION_GROUPS.get("Route 130", set()), + *LOCATION_GROUPS.get("Route 131", set()), + *LOCATION_GROUPS.get("Route 132", set()), + *LOCATION_GROUPS.get("Route 133", set()), + *LOCATION_GROUPS.get("Route 134", set()), +} diff --git a/worlds/pokemon_emerald/items.py b/worlds/pokemon_emerald/items.py index 436db771d396..922bbbc0dbfb 100644 --- a/worlds/pokemon_emerald/items.py +++ b/worlds/pokemon_emerald/items.py @@ -1,7 +1,7 @@ """ Classes and functions related to AP items for Pokemon Emerald """ -from typing import Dict, FrozenSet, Optional +from typing import Dict, FrozenSet, Set, Optional from BaseClasses import Item, ItemClassification @@ -46,30 +46,6 @@ def create_item_label_to_code_map() -> Dict[str, int]: return label_to_code_map -ITEM_GROUPS = { - "Badges": { - "Stone Badge", "Knuckle Badge", - "Dynamo Badge", "Heat Badge", - "Balance Badge", "Feather Badge", - "Mind Badge", "Rain Badge", - }, - "HMs": { - "HM01 Cut", "HM02 Fly", - "HM03 Surf", "HM04 Strength", - "HM05 Flash", "HM06 Rock Smash", - "HM07 Waterfall", "HM08 Dive", - }, - "HM01": {"HM01 Cut"}, - "HM02": {"HM02 Fly"}, - "HM03": {"HM03 Surf"}, - "HM04": {"HM04 Strength"}, - "HM05": {"HM05 Flash"}, - "HM06": {"HM06 Rock Smash"}, - "HM07": {"HM07 Waterfall"}, - "HM08": {"HM08 Dive"}, -} - - def get_item_classification(item_code: int) -> ItemClassification: """ Returns the item classification for a given AP item id (code) diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 9123690bead7..473c189166be 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -1,59 +1,17 @@ """ Classes and functions related to AP locations for Pokemon Emerald """ -from typing import TYPE_CHECKING, Dict, Optional, FrozenSet, Iterable +from typing import TYPE_CHECKING, Dict, Optional, Set from BaseClasses import Location, Region -from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, data +from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, LocationCategory, data from .items import offset_item_value if TYPE_CHECKING: from . import PokemonEmeraldWorld -LOCATION_GROUPS = { - "Badges": { - "Rustboro Gym - Stone Badge", - "Dewford Gym - Knuckle Badge", - "Mauville Gym - Dynamo Badge", - "Lavaridge Gym - Heat Badge", - "Petalburg Gym - Balance Badge", - "Fortree Gym - Feather Badge", - "Mossdeep Gym - Mind Badge", - "Sootopolis Gym - Rain Badge", - }, - "Gym TMs": { - "Rustboro Gym - TM39 from Roxanne", - "Dewford Gym - TM08 from Brawly", - "Mauville Gym - TM34 from Wattson", - "Lavaridge Gym - TM50 from Flannery", - "Petalburg Gym - TM42 from Norman", - "Fortree Gym - TM40 from Winona", - "Mossdeep Gym - TM04 from Tate and Liza", - "Sootopolis Gym - TM03 from Juan", - }, - "Trick House": { - "Trick House Puzzle 1 - Item", - "Trick House Puzzle 2 - Item 1", - "Trick House Puzzle 2 - Item 2", - "Trick House Puzzle 3 - Item 1", - "Trick House Puzzle 3 - Item 2", - "Trick House Puzzle 4 - Item", - "Trick House Puzzle 6 - Item", - "Trick House Puzzle 7 - Item", - "Trick House Puzzle 8 - Item", - "Trick House Puzzle 1 - Reward", - "Trick House Puzzle 2 - Reward", - "Trick House Puzzle 3 - Reward", - "Trick House Puzzle 4 - Reward", - "Trick House Puzzle 5 - Reward", - "Trick House Puzzle 6 - Reward", - "Trick House Puzzle 7 - Reward", - } -} - - VISITED_EVENT_NAME_TO_ID = { "EVENT_VISITED_LITTLEROOT_TOWN": 0, "EVENT_VISITED_OLDALE_TOWN": 1, @@ -80,7 +38,7 @@ class PokemonEmeraldLocation(Location): game: str = "Pokemon Emerald" item_address: Optional[int] default_item_code: Optional[int] - tags: FrozenSet[str] + key: Optional[str] def __init__( self, @@ -88,13 +46,13 @@ def __init__( name: str, address: Optional[int], parent: Optional[Region] = None, + key: Optional[str] = None, item_address: Optional[int] = None, - default_item_value: Optional[int] = None, - tags: FrozenSet[str] = frozenset()) -> None: + default_item_value: Optional[int] = None) -> None: super().__init__(player, name, address, parent) self.default_item_code = None if default_item_value is None else offset_item_value(default_item_value) self.item_address = item_address - self.tags = tags + self.key = key def offset_flag(flag: int) -> int: @@ -115,16 +73,14 @@ def reverse_offset_flag(location_id: int) -> int: return location_id - BASE_OFFSET -def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, Region], tags: Iterable[str]) -> None: +def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str, Region], categories: Set[LocationCategory]) -> None: """ Iterates through region data and adds locations to the multiworld if those locations include any of the provided tags. """ - tags = set(tags) - for region_name, region_data in data.regions.items(): region = regions[region_name] - filtered_locations = [loc for loc in region_data.locations if len(tags & data.locations[loc].tags) > 0] + filtered_locations = [loc for loc in region_data.locations if data.locations[loc].category in categories] for location_name in filtered_locations: location_data = data.locations[location_name] @@ -144,9 +100,9 @@ def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, location_data.label, location_id, region, + location_name, location_data.address, - location_data.default_item, - location_data.tags + location_data.default_item ) region.locations.append(location) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 5f83686ebeec..b8d1efb1a98d 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -6,7 +6,8 @@ from BaseClasses import CollectionState from worlds.generic.Rules import add_rule, set_rule -from .data import NATIONAL_ID_TO_SPECIES_ID, NUM_REAL_SPECIES, data +from .data import LocationCategory, NATIONAL_ID_TO_SPECIES_ID, NUM_REAL_SPECIES, data +from .locations import PokemonEmeraldLocation from .options import DarkCavesRequireFlash, EliteFourRequirement, NormanRequirement, Goal if TYPE_CHECKING: @@ -23,7 +24,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: state.has(hm, world.player) and state.has_all(badges, world.player) else: hm_rules[hm] = lambda state, hm=hm, badges=badges: \ - state.has(hm, world.player) and state.has_group_unique("Badges", world.player, badges) + state.has(hm, world.player) and state.has_group_unique("Badge", world.player, badges) def has_acro_bike(state: CollectionState): return state.has("Acro Bike", world.player) @@ -236,11 +237,11 @@ def get_location(location: str): if world.options.norman_requirement == NormanRequirement.option_badges: set_rule( get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"), - lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value) + lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value) ) set_rule( get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"), - lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value) + lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value) ) else: set_rule( @@ -1506,7 +1507,7 @@ def get_location(location: str): if world.options.elite_four_requirement == EliteFourRequirement.option_badges: set_rule( get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"), - lambda state: state.has_group_unique("Badges", world.player, world.options.elite_four_count.value) + lambda state: state.has_group_unique("Badge", world.player, world.options.elite_four_count.value) ) else: set_rule( @@ -1659,7 +1660,8 @@ def get_location(location: str): # Add Itemfinder requirement to hidden items if world.options.require_itemfinder: for location in world.multiworld.get_locations(world.player): - if location.tags is not None and "HiddenItem" in location.tags: + assert isinstance(location, PokemonEmeraldLocation) + if location.key is not None and data.locations[location.key].category == LocationCategory.HIDDEN_ITEM: add_rule( location, lambda state: state.has("Itemfinder", world.player) diff --git a/worlds/pokemon_emerald/sanity_check.py b/worlds/pokemon_emerald/sanity_check.py index 24eb768bfbc5..048b19b46919 100644 --- a/worlds/pokemon_emerald/sanity_check.py +++ b/worlds/pokemon_emerald/sanity_check.py @@ -5,8 +5,6 @@ import logging from typing import List -from .data import load_json_data, data - _IGNORABLE_LOCATIONS = frozenset({ "HIDDEN_ITEM_TRICK_HOUSE_NUGGET", # Is permanently mssiable and has special behavior that sets the flag early @@ -247,12 +245,29 @@ }) +def validate_group_maps() -> bool: + from .data import data + from .groups import _LOCATION_GROUP_MAPS + + failed = False + + for group_name, map_set in _LOCATION_GROUP_MAPS.items(): + for map_name in map_set: + if map_name not in data.maps: + failed = True + logging.error("Pokemon Emerald: Map named %s in location group %s does not exist", map_name, group_name) + + return not failed + + def validate_regions() -> bool: """ Verifies that Emerald's data doesn't have duplicate or missing regions/warps/locations. Meant to catch problems during development like forgetting to add a new location or incorrectly splitting a region. """ + from .data import load_json_data, data + extracted_data_json = load_json_data("extracted_data.json") error_messages: List[str] = [] warn_messages: List[str] = [] From 05aa96a33573b97c93a2e252994699050e262252 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:07:14 +0100 Subject: [PATCH 027/112] CI: use py3.12 for the linux and windows builds (#4284) * CI: use py3.12 for the linux build * CI: use py3.12 for the windows build --- .github/workflows/build.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c013172ea034..a5a2f52f3222 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip @@ -111,10 +111,10 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f8651d408e7..b28ec8733408 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,10 +44,10 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract From d36c9834612a35cdeb84f43e6609ff5021c5f235 Mon Sep 17 00:00:00 2001 From: Benjamin S Wolf Date: Fri, 29 Nov 2024 11:40:02 -0800 Subject: [PATCH 028/112] Core: Log warnings at call site, not Utils itself (#4229) --- Utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Utils.py b/Utils.py index 4e8d7fc427e0..f5c6ca6414f5 100644 --- a/Utils.py +++ b/Utils.py @@ -858,11 +858,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non task.add_done_callback(_faf_tasks.discard) -def deprecate(message: str): +def deprecate(message: str, add_stacklevels: int = 0): if __debug__: raise Exception(message) - import warnings - warnings.warn(message) + warnings.warn(message, stacklevel=2 + add_stacklevels) class DeprecateDict(dict): @@ -876,10 +875,9 @@ def __init__(self, message: str, error: bool = False) -> None: def __getitem__(self, item: Any) -> Any: if self.should_error: - deprecate(self.log_message) + deprecate(self.log_message, add_stacklevels=1) elif __debug__: - import warnings - warnings.warn(self.log_message) + warnings.warn(self.log_message, stacklevel=2) return super().__getitem__(item) From ed4e44b99403fedb27ab933e50068192bf2a4e7a Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:41:26 -0500 Subject: [PATCH 029/112] Stardew Valley: Remove some events for a slight performance increase (#4085) --- worlds/stardew_valley/__init__.py | 15 ------- worlds/stardew_valley/logic/building_logic.py | 11 +++-- worlds/stardew_valley/logic/money_logic.py | 13 ++++-- worlds/stardew_valley/logic/shipping_logic.py | 5 +-- .../logic/special_order_logic.py | 7 ++-- worlds/stardew_valley/rules.py | 40 ++++++++----------- .../stardew_rule/rule_explain.py | 20 +++++----- .../strings/ap_names/event_names.py | 4 -- .../test/rules/TestBuildings.py | 9 ----- 9 files changed, 48 insertions(+), 76 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 44306011361c..01ca56531f00 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -206,25 +206,10 @@ def precollect_farm_type_items(self): self.multiworld.push_precollected(self.create_starting_item("Progressive Coop")) def setup_player_events(self): - self.setup_construction_events() - self.setup_quest_events() self.setup_action_events() self.setup_logic_events() - def setup_construction_events(self): - can_construct_buildings = LocationData(None, RegionName.carpenter, Event.can_construct_buildings) - self.create_event_location(can_construct_buildings, True_(), Event.can_construct_buildings) - - def setup_quest_events(self): - start_dark_talisman_quest = LocationData(None, RegionName.railroad, Event.start_dark_talisman_quest) - self.create_event_location(start_dark_talisman_quest, self.logic.wallet.has_rusty_key(), Event.start_dark_talisman_quest) - def setup_action_events(self): - can_ship_event = LocationData(None, LogicRegion.shipping, Event.can_ship_items) - self.create_event_location(can_ship_event, true_, Event.can_ship_items) - can_shop_pierre_event = LocationData(None, RegionName.pierre_store, Event.can_shop_at_pierre) - self.create_event_location(can_shop_pierre_event, true_, Event.can_shop_at_pierre) - spring_farming = LocationData(None, LogicRegion.spring_farming, Event.spring_farming) self.create_event_location(spring_farming, true_, Event.spring_farming) summer_farming = LocationData(None, LogicRegion.summer_farming, Event.summer_farming) diff --git a/worlds/stardew_valley/logic/building_logic.py b/worlds/stardew_valley/logic/building_logic.py index 4611eba37d64..b4eff4399385 100644 --- a/worlds/stardew_valley/logic/building_logic.py +++ b/worlds/stardew_valley/logic/building_logic.py @@ -1,3 +1,4 @@ +from functools import cached_property from typing import Dict, Union from Utils import cache_self1 @@ -8,12 +9,12 @@ from .region_logic import RegionLogicMixin from ..options import BuildingProgression from ..stardew_rule import StardewRule, True_, False_, Has -from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.fish_names import WaterItem from ..strings.material_names import Material from ..strings.metal_names import MetalBar +from ..strings.region_names import Region has_group = "building" @@ -60,7 +61,7 @@ def has_building(self, building: str) -> StardewRule: return True_() return self.logic.received(building) - carpenter_rule = self.logic.received(Event.can_construct_buildings) + carpenter_rule = self.logic.building.can_construct_buildings if not self.options.building_progression & BuildingProgression.option_progressive: return Has(building, self.registry.building_rules, has_group) & carpenter_rule @@ -75,6 +76,10 @@ def has_building(self, building: str) -> StardewRule: building = " ".join(["Progressive", *building.split(" ")[1:]]) return self.logic.received(building, count) & carpenter_rule + @cached_property + def can_construct_buildings(self) -> StardewRule: + return self.logic.region.can_reach(Region.carpenter) + @cache_self1 def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level < 1: @@ -83,7 +88,7 @@ def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level > 3: return False_() - carpenter_rule = self.logic.received(Event.can_construct_buildings) + carpenter_rule = self.logic.building.can_construct_buildings if self.options.building_progression & BuildingProgression.option_progressive: return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level) diff --git a/worlds/stardew_valley/logic/money_logic.py b/worlds/stardew_valley/logic/money_logic.py index 73c5291af082..85370273c987 100644 --- a/worlds/stardew_valley/logic/money_logic.py +++ b/worlds/stardew_valley/logic/money_logic.py @@ -1,3 +1,4 @@ +import typing from typing import Union from Utils import cache_self1 @@ -11,10 +12,14 @@ from ..data.shop import ShopSource from ..options import SpecialOrderLocations from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_ -from ..strings.ap_names.event_names import Event from ..strings.currency_names import Currency from ..strings.region_names import Region, LogicRegion +if typing.TYPE_CHECKING: + from .shipping_logic import ShippingLogicMixin + + assert ShippingLogicMixin + qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems", "20 Qi Gems", "15 Qi Gems", "10 Qi Gems") @@ -26,7 +31,7 @@ def __init__(self, *args, **kwargs): class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin, -GrindLogicMixin]]): +GrindLogicMixin, 'ShippingLogicMixin']]): @cache_self1 def can_have_earned_total(self, amount: int) -> StardewRule: @@ -37,7 +42,7 @@ def can_have_earned_total(self, amount: int) -> StardewRule: willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing)) clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5)) robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods)) - shipping_rule = self.logic.received(Event.can_ship_items) + shipping_rule = self.logic.shipping.can_use_shipping_bin if amount < 2000: selling_any_rule = pierre_rule | willy_rule | clint_rule | robin_rule | shipping_rule @@ -50,7 +55,7 @@ def can_have_earned_total(self, amount: int) -> StardewRule: if amount < 10000: return shipping_rule - seed_rules = self.logic.received(Event.can_shop_at_pierre) + seed_rules = self.logic.region.can_reach(Region.pierre_store) if amount < 40000: return shipping_rule & seed_rules diff --git a/worlds/stardew_valley/logic/shipping_logic.py b/worlds/stardew_valley/logic/shipping_logic.py index 8d545e219627..e9f2258172e6 100644 --- a/worlds/stardew_valley/logic/shipping_logic.py +++ b/worlds/stardew_valley/logic/shipping_logic.py @@ -11,7 +11,6 @@ from ..options import ExcludeGingerIsland, Shipsanity from ..options import SpecialOrderLocations from ..stardew_rule import StardewRule -from ..strings.ap_names.event_names import Event from ..strings.building_names import Building @@ -29,7 +28,7 @@ def can_use_shipping_bin(self) -> StardewRule: @cache_self1 def can_ship(self, item: str) -> StardewRule: - return self.logic.received(Event.can_ship_items) & self.logic.has(item) + return self.logic.shipping.can_use_shipping_bin & self.logic.has(item) def can_ship_everything(self) -> StardewRule: shipsanity_prefix = "Shipsanity: " @@ -49,7 +48,7 @@ def can_ship_everything(self) -> StardewRule: def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule: if self.options.shipsanity == Shipsanity.option_none: - return self.can_ship_everything() + return self.logic.shipping.can_ship_everything() rules = [self.logic.building.has_building(Building.shipping_bin)] diff --git a/worlds/stardew_valley/logic/special_order_logic.py b/worlds/stardew_valley/logic/special_order_logic.py index 65497df477b8..8bcd78d7d26e 100644 --- a/worlds/stardew_valley/logic/special_order_logic.py +++ b/worlds/stardew_valley/logic/special_order_logic.py @@ -21,7 +21,6 @@ from ..content.vanilla.qi_board import qi_board_content_pack from ..stardew_rule import StardewRule, Has, false_ from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.event_names import Event from ..strings.ap_names.transport_names import Transportation from ..strings.artisan_good_names import ArtisanGood from ..strings.crop_names import Vegetable, Fruit @@ -61,7 +60,7 @@ def initialize_rules(self): SpecialOrder.gifts_for_george: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek), SpecialOrder.fragments_of_the_past: self.logic.monster.can_kill(Monster.skeleton), SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg), - SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items), + SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_use_shipping_bin, SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot, SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)), SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(), @@ -94,12 +93,12 @@ def initialize_rules(self): self.update_rules({ SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & - self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items), + self.logic.has(Machine.seed_maker) & self.logic.shipping.can_use_shipping_bin, SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), - SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) & + SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.shipping.can_use_shipping_bin & (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index eda2d4377e09..e7107e89f948 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -27,7 +27,6 @@ from .stardew_rule.rule_explain import explain from .strings.ap_names.ap_option_names import OptionName from .strings.ap_names.community_upgrade_names import CommunityUpgrade -from .strings.ap_names.event_names import Event from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes from .strings.ap_names.transport_names import Transportation from .strings.artisan_good_names import ArtisanGood @@ -251,7 +250,8 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, Entrance.enter_witch_warp_cave, logic.quest.has_dark_talisman() | (logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_witch_hut, (logic.has(ArtisanGood.void_mayonnaise) | logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_mutant_bug_lair, - (logic.received(Event.start_dark_talisman_quest) & logic.relationship.can_meet(NPC.krobus)) | logic.mod.magic.can_blink()) + (logic.wallet.has_rusty_key() & logic.region.can_reach(Region.railroad) & logic.relationship.can_meet( + NPC.krobus)) | logic.mod.magic.can_blink()) set_entrance_rule(multiworld, player, Entrance.enter_casino, logic.quest.has_club_card()) set_bedroom_entrance_rules(logic, multiworld, player, world_options) @@ -307,8 +307,7 @@ def set_mines_floor_entrance_rules(logic, multiworld, player): rule = logic.mine.has_mine_elevator_to_floor(floor - 10) if floor == 5 or floor == 45 or floor == 85: rule = rule & logic.mine.can_progress_in_the_mines_from_floor(floor) - entrance = multiworld.get_entrance(dig_to_mines_floor(floor), player) - MultiWorldRules.set_rule(entrance, rule) + set_entrance_rule(multiworld, player, dig_to_mines_floor(floor), rule) def set_skull_cavern_floor_entrance_rules(logic, multiworld, player): @@ -316,8 +315,7 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player): rule = logic.mod.elevator.has_skull_cavern_elevator_to_floor(floor - 25) if floor == 25 or floor == 75 or floor == 125: rule = rule & logic.mine.can_progress_in_the_skull_cavern_from_floor(floor) - entrance = multiworld.get_entrance(dig_to_skull_floor(floor), player) - MultiWorldRules.set_rule(entrance, rule) + set_entrance_rule(multiworld, player, dig_to_skull_floor(floor), rule) def set_blacksmith_entrance_rules(logic, multiworld, player): @@ -346,9 +344,8 @@ def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewVa def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str): - material_entrance = multiworld.get_entrance(entrance_name, player) upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material]) - MultiWorldRules.set_rule(material_entrance, upgrade_rule) + set_entrance_rule(multiworld, player, entrance_name, upgrade_rule) def set_festival_entrance_rules(logic, multiworld, player): @@ -880,25 +877,19 @@ def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player), - logic.received(Wallet.skull_key)) + play_junimo_kart_rule = logic.received(Wallet.skull_key) + if world_options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling: + set_entrance_rule(multiworld, player, Entrance.play_junimo_kart, play_junimo_kart_rule) return - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player), - logic.has("Junimo Kart Small Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_2, player), - logic.has("Junimo Kart Medium Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player), - logic.has("Junimo Kart Big Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player), - logic.has("Junimo Kart Max Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player), - logic.has("JotPK Small Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_2, player), - logic.has("JotPK Medium Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_3, player), - logic.has("JotPK Big Buff")) + set_entrance_rule(multiworld, player, Entrance.play_junimo_kart, play_junimo_kart_rule & logic.has("Junimo Kart Small Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_2, logic.has("Junimo Kart Medium Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_3, logic.has("Junimo Kart Big Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_4, logic.has("Junimo Kart Max Buff")) + set_entrance_rule(multiworld, player, Entrance.play_journey_of_the_prairie_king, logic.has("JotPK Small Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_jotpk_world_2, logic.has("JotPK Medium Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_jotpk_world_3, logic.has("JotPK Big Buff")) MultiWorldRules.add_rule(multiworld.get_location("Journey of the Prairie King Victory", player), logic.has("JotPK Max Buff")) @@ -1049,6 +1040,7 @@ def set_entrance_rule(multiworld, player, entrance: str, rule: StardewRule): potentially_required_regions = look_for_indirect_connection(rule) if potentially_required_regions: for region in potentially_required_regions: + logger.debug(f"Registering indirect condition for {region} -> {entrance}") multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py index a9767c7b72d5..2e2b9c959d7f 100644 --- a/worlds/stardew_valley/stardew_rule/rule_explain.py +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -4,7 +4,7 @@ from functools import cached_property, singledispatch from typing import Iterable, Set, Tuple, List, Optional -from BaseClasses import CollectionState +from BaseClasses import CollectionState, Location, Entrance from worlds.generic.Rules import CollectionRule from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_ @@ -12,10 +12,10 @@ @dataclass class RuleExplanation: rule: StardewRule - state: CollectionState + state: CollectionState = field(repr=False, hash=False) expected: bool sub_rules: Iterable[StardewRule] = field(default_factory=list) - explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set) + explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set, repr=False, hash=False) current_rule_explored: bool = False def __post_init__(self): @@ -38,13 +38,6 @@ def __str__(self, depth=0): if i.result is not self.expected else i.summary(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - def __repr__(self, depth=0): - if not self.sub_rules: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - @cached_property def result(self) -> bool: try: @@ -134,6 +127,10 @@ def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[T access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] else: access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + elif spot.access_rule == Location.access_rule: + # Sometime locations just don't have an access rule and all the relevant logic is in the parent region. + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + elif rule.resolution_hint == 'Entrance': spot = state.multiworld.get_entrance(rule.spot, rule.player) @@ -143,6 +140,9 @@ def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[T access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] else: access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + elif spot.access_rule == Entrance.access_rule: + # Sometime entrances just don't have an access rule and all the relevant logic is in the parent region. + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] else: spot = state.multiworld.get_region(rule.spot, rule.player) diff --git a/worlds/stardew_valley/strings/ap_names/event_names.py b/worlds/stardew_valley/strings/ap_names/event_names.py index 88f9715abc65..449bb6720964 100644 --- a/worlds/stardew_valley/strings/ap_names/event_names.py +++ b/worlds/stardew_valley/strings/ap_names/event_names.py @@ -8,10 +8,6 @@ def event(name: str): class Event: victory = event("Victory") - can_construct_buildings = event("Can Construct Buildings") - start_dark_talisman_quest = event("Start Dark Talisman Quest") - can_ship_items = event("Can Ship Items") - can_shop_at_pierre = event("Can Shop At Pierre's") spring_farming = event("Spring Farming") summer_farming = event("Summer Farming") fall_farming = event("Fall Farming") diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py index 2c276d8b5cbe..cacd6ea381b6 100644 --- a/worlds/stardew_valley/test/rules/TestBuildings.py +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -23,10 +23,6 @@ def test_big_coop_blueprint(self): self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - self.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=True) - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=False) self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") @@ -35,7 +31,6 @@ def test_deluxe_coop_blueprint(self): self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) self.collect_lots_of_money() - self.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=True) self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=True) @@ -53,10 +48,6 @@ def test_big_shed_blueprint(self): self.assertFalse(big_shed_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - self.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=True) - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - self.multiworld.state.collect(self.create_item("Progressive Shed"), prevent_sweep=True) self.assertTrue(big_shed_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") From 1454bacfddca12cf324198e27963f063dcc9c12e Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 29 Nov 2024 14:43:33 -0500 Subject: [PATCH 030/112] HK: better error messaging for charm plando (#3907) --- worlds/hk/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index fc8eae1c0aa3..e17abbb7ae47 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -300,7 +300,7 @@ class PlandoCharmCosts(OptionDict): display_name = "Charm Notch Cost Plando" valid_keys = frozenset(charm_names) schema = Schema({ - Optional(name): And(int, lambda n: 6 >= n >= 0) for name in charm_names + Optional(name): And(int, lambda n: 6 >= n >= 0, error="Charm costs must be integers in the range 0-6.") for name in charm_names }) def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]: From a5231a27cc1d25e55f58fb847d3148a8db346523 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:45:10 +0100 Subject: [PATCH 031/112] Yacht Dice: Mark YachtWeights.py as "linguist-generated" (#3898) This means its diff will be collapsed by default on PRs that change it, because it is an "auto generated" file that does not need to be looked at by reviewers --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 537a05f68b67..5ab537933405 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ worlds/blasphemous/region_data.py linguist-generated=true +worlds/yachtdice/YachtWeights.py linguist-generated=true From b605fb10323c8d1ace0465ece41db2b0eb2d0b2e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:45:44 +0100 Subject: [PATCH 032/112] The Witness: Make Elevators Come To You an OptionSet (#4000) * Split elevators come to you * . * unit test * mypy stuff * Fine. I'll fix the fcking commented out code. Happy? * ruff * """""Backwards compatibility""""" * ruff * make it look better * # * fix presets * fix a unit test * Make that explicit in the code * Improve description --- .../Door_Shuffle/Elevators_Come_To_You.txt | 11 --- worlds/witness/data/utils.py | 4 -- worlds/witness/options.py | 42 ++++++++++-- worlds/witness/player_logic.py | 26 ++++++- worlds/witness/presets.py | 9 ++- worlds/witness/test/test_auto_elevators.py | 68 +++++++------------ .../witness/test/test_roll_other_options.py | 3 +- 7 files changed, 94 insertions(+), 69 deletions(-) delete mode 100644 worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt diff --git a/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt b/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt deleted file mode 100644 index 78d245f9f0b5..000000000000 --- a/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt +++ /dev/null @@ -1,11 +0,0 @@ -New Connections: -Quarry - Quarry Elevator - TrueOneWay -Outside Quarry - Quarry Elevator - TrueOneWay -Outside Bunker - Bunker Elevator - TrueOneWay -Outside Swamp - Swamp Long Bridge - TrueOneWay -Swamp Near Boat - Swamp Long Bridge - TrueOneWay -Town Red Rooftop - Town Maze Rooftop - TrueOneWay - - -Requirement Changes: -0x035DE - 0x17E2B - True \ No newline at end of file diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 737daff70fae..190c00dc283b 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -204,10 +204,6 @@ def get_caves_except_path_to_challenge_exclusion_list() -> List[str]: return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt") -def get_elevators_come_to_you() -> List[str]: - return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt") - - def get_entity_hunt() -> List[str]: return get_adjustment_file("settings/Entity_Hunt.txt") diff --git a/worlds/witness/options.py b/worlds/witness/options.py index bb935388e3c7..b5c15e242f10 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -2,7 +2,18 @@ from schema import And, Schema -from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility +from Options import ( + Choice, + DefaultOnToggle, + OptionDict, + OptionError, + OptionGroup, + OptionSet, + PerGameCommonOptions, + Range, + Toggle, + Visibility, +) from .data import static_logic as static_witness_logic from .data.item_definition_classes import ItemCategory, WeightedItemDefinition @@ -294,12 +305,33 @@ class ChallengeLasers(Range): default = 11 -class ElevatorsComeToYou(Toggle): +class ElevatorsComeToYou(OptionSet): """ - If on, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them. - This does actually affect logic as it allows unintended backwards / early access into these areas. + In vanilla, some bridges/elevators come to you if you walk up to them when they are not currently there. + However, there are some that don't. Notably, this prevents Quarry Elevator from being a logical access method into Quarry, because you can send it away without riding ot and then permanently be locked out of using it. + + This option allows you to change specific elevators/bridges to "come to you" as well. + + - Quarry Elevator: Makes the Quarry Elevator come down when you approach it from lower Quarry and back up when you approach it from above + - Swamp Long Bridge: Rotates the side you approach it from towards you, but also rotates the other side away + - Bunker Elevator: Makes the Bunker Elevator come to any floor that you approach it from, meaning it can be accessed from the roof immediately """ - display_name = "All Bridges & Elevators come to you" + + # Used to be a toggle + @classmethod + def from_text(cls, text: str): + if text.lower() in {"off", "0", "false", "none", "null", "no"}: + raise OptionError('elevators_come_to_you is an OptionSet now. The equivalent of "false" is {}') + if text.lower() in {"on", "1", "true", "yes"}: + raise OptionError( + f'elevators_come_to_you is an OptionSet now. The equivalent of "true" is {set(cls.valid_keys)}' + ) + return super().from_text(text) + + display_name = "Elevators come to you" + + valid_keys = frozenset({"Quarry Elevator", "Swamp Long Bridge", "Bunker Elevator"}) + default = frozenset({"Quarry Elevator"}) class TrapPercentage(Range): diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index fd86679844a7..ec4ea066e579 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -34,7 +34,6 @@ get_discard_exclusion_list, get_early_caves_list, get_early_caves_start_list, - get_elevators_come_to_you, get_entity_hunt, get_ep_all_individual, get_ep_easy, @@ -626,8 +625,29 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if world.options.early_caves == "add_to_pool" and not remote_doors: adjustment_linesets_in_order.append(get_early_caves_list()) - if world.options.elevators_come_to_you: - adjustment_linesets_in_order.append(get_elevators_come_to_you()) + if "Quarry Elevator" in world.options.elevators_come_to_you: + adjustment_linesets_in_order.append([ + "New Connections:", + "Quarry - Quarry Elevator - TrueOneWay", + "Outside Quarry - Quarry Elevator - TrueOneWay", + ]) + if "Bunker Elevator" in world.options.elevators_come_to_you: + adjustment_linesets_in_order.append([ + "New Connections:", + "Outside Bunker - Bunker Elevator - TrueOneWay", + ]) + if "Swamp Long Bridge" in world.options.elevators_come_to_you: + adjustment_linesets_in_order.append([ + "New Connections:", + "Outside Swamp - Swamp Long Bridge - TrueOneWay", + "Swamp Near Boat - Swamp Long Bridge - TrueOneWay", + "Requirement Changes:", + "0x035DE - 0x17E2B - True", # Swamp Purple Sand Bottom EP + ]) + # if "Town Maze Rooftop Bridge" in world.options.elevators_come_to_you: + # adjustment_linesets_in_order.append([ + # "New Connections:" + # "Town Red Rooftop - Town Maze Rooftop - TrueOneWay" if world.options.victory_condition == "panel_hunt": adjustment_linesets_in_order.append(get_entity_hunt()) diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 8993048065f4..687d74f771cb 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -35,7 +35,8 @@ "challenge_lasers": 11, "early_caves": EarlyCaves.option_off, - "elevators_come_to_you": False, + + "elevators_come_to_you": ElevatorsComeToYou.default, "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": PuzzleSkipAmount.default, @@ -73,7 +74,8 @@ "challenge_lasers": 9, "early_caves": EarlyCaves.option_off, - "elevators_come_to_you": False, + + "elevators_come_to_you": ElevatorsComeToYou.default, "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": 15, @@ -111,7 +113,8 @@ "challenge_lasers": 9, "early_caves": EarlyCaves.option_off, - "elevators_come_to_you": True, + + "elevators_come_to_you": ElevatorsComeToYou.valid_keys, "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": 15, diff --git a/worlds/witness/test/test_auto_elevators.py b/worlds/witness/test/test_auto_elevators.py index 16b1b5a56d37..f91943e85577 100644 --- a/worlds/witness/test/test_auto_elevators.py +++ b/worlds/witness/test/test_auto_elevators.py @@ -1,49 +1,25 @@ -from ..test import WitnessMultiworldTestBase, WitnessTestBase - - -class TestElevatorsComeToYou(WitnessTestBase): - options = { - "elevators_come_to_you": True, - "shuffle_doors": "mixed", - "shuffle_symbols": False, - } - - def test_bunker_laser(self) -> None: - """ - In elevators_come_to_you, Bunker can be entered from the back. - This means that you can access the laser with just Bunker Elevator Control (Panel). - It also means that you can, for example, access UV Room with the Control and the Elevator Room Entry Door. - """ - - self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player)) - - self.collect_by_name("Bunker Elevator Control (Panel)") - - self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player)) - self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player)) - - self.collect_by_name("Bunker Elevator Room Entry (Door)") - self.collect_by_name("Bunker Drop-Down Door Controls (Panel)") - - self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player)) +from ..test import WitnessMultiworldTestBase class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase): options_per_world = [ { - "elevators_come_to_you": False, + "elevators_come_to_you": {}, }, { - "elevators_come_to_you": True, + "elevators_come_to_you": {"Quarry Elevator", "Swamp Long Bridge", "Bunker Elevator"}, }, { - "elevators_come_to_you": False, + "elevators_come_to_you": {} }, ] common_options = { "shuffle_symbols": False, "shuffle_doors": "panels", + "shuffle_boat": True, + "shuffle_EPs": "individual", + "obelisk_keys": False, } def test_correct_access_per_player(self) -> None: @@ -53,14 +29,22 @@ def test_correct_access_per_player(self) -> None: (This is essentially a "does connection info bleed over" test). """ - self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1)) - self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2)) - self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3)) - - self.collect_by_name(["Bunker Elevator Control (Panel)"], 1) - self.collect_by_name(["Bunker Elevator Control (Panel)"], 2) - self.collect_by_name(["Bunker Elevator Control (Panel)"], 3) - - self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1)) - self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2)) - self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3)) + combinations = [ + ("Quarry Elevator Control (Panel)", "Quarry Boathouse Intro Left"), + ("Swamp Long Bridge (Panel)", "Swamp Long Bridge Side EP"), + ("Bunker Elevator Control (Panel)", "Bunker Laser Panel"), + ] + + for item, location in combinations: + with self.subTest(f"Test that {item} only locks {location} for player 2"): + self.assertFalse(self.multiworld.state.can_reach_location(location, 1)) + self.assertFalse(self.multiworld.state.can_reach_location(location, 2)) + self.assertFalse(self.multiworld.state.can_reach_location(location, 3)) + + self.collect_by_name(item, 1) + self.collect_by_name(item, 2) + self.collect_by_name(item, 3) + + self.assertFalse(self.multiworld.state.can_reach_location(location, 1)) + self.assertTrue(self.multiworld.state.can_reach_location(location, 2)) + self.assertFalse(self.multiworld.state.can_reach_location(location, 3)) diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py index 7473716e06e6..0429b097eac3 100644 --- a/worlds/witness/test/test_roll_other_options.py +++ b/worlds/witness/test/test_roll_other_options.py @@ -1,3 +1,4 @@ +from ..options import ElevatorsComeToYou from ..test import WitnessTestBase # These are just some random options combinations, just to catch whether I broke anything obvious @@ -19,7 +20,7 @@ class TestExpertNonRandomizedEPs(WitnessTestBase): class TestVanillaAutoElevatorsPanels(WitnessTestBase): options = { "puzzle_randomization": "none", - "elevators_come_to_you": True, + "elevators_come_to_you": ElevatorsComeToYou.valid_keys - ElevatorsComeToYou.default, # Opposite of default "shuffle_doors": "panels", "victory_condition": "mountain_box_short", "early_caves": True, From 77d35b95e242fa15c60739262938d70be27fa8d7 Mon Sep 17 00:00:00 2001 From: Ehseezed <97066152+Ehseezed@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:46:12 -0600 Subject: [PATCH 033/112] Timespinner: Update AP to have parity with standalone options (#3805) --- worlds/timespinner/Items.py | 8 ++++++-- worlds/timespinner/Locations.py | 4 ++-- worlds/timespinner/LogicExtensions.py | 3 +++ worlds/timespinner/Options.py | 20 +++++++++++++++++--- worlds/timespinner/__init__.py | 10 ++++++++++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index 45c67c254736..3beead95153b 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -138,7 +138,7 @@ class ItemData(NamedTuple): 'Elevator Keycard': ItemData('Relic', 1337125, progression=True), 'Jewelry Box': ItemData('Relic', 1337126, useful=True), 'Goddess Brooch': ItemData('Relic', 1337127), - 'Wyrm Brooch': ItemData('Relic', 1337128), + 'Wyrm Brooch': ItemData('Relic', 1337128), 'Greed Brooch': ItemData('Relic', 1337129), 'Eternal Brooch': ItemData('Relic', 1337130), 'Blue Orb': ItemData('Orb Melee', 1337131), @@ -199,7 +199,11 @@ class ItemData(NamedTuple): 'Chaos Trap': ItemData('Trap', 1337186, 0, trap=True), 'Neurotoxin Trap': ItemData('Trap', 1337187, 0, trap=True), 'Bee Trap': ItemData('Trap', 1337188, 0, trap=True), - # 1337189 - 1337248 Reserved + 'Laser Access A': ItemData('Relic', 1337189, progression=True), + 'Laser Access I': ItemData('Relic', 1337191, progression=True), + 'Laser Access M': ItemData('Relic', 1337192, progression=True), + 'Throw Stun Trap': ItemData('Trap', 1337193, 0, trap=True), + # 1337194 - 1337248 Reserved 'Max Sand': ItemData('Stat', 1337249, 14) } diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 2423e06bb010..93ac6ccb98c7 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -71,8 +71,8 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio LocationData('Skeleton Shaft', 'Sealed Caves (Xarion): Skeleton', 1337044), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Shroom jump room', 1337045, logic.has_timestop), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Double shroom room', 1337046), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Mini jackpot room', 1337047, logic.has_forwarddash_doublejump), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below mini jackpot room', 1337048), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Jacksquat room', 1337047, logic.has_forwarddash_doublejump), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below Jacksquat room', 1337048), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, logic.can_break_walls), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Bottom left room', 1337050), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, logic.has_doublejump), diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index 6c9cb3f684a0..2a0a358737f7 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -22,6 +22,7 @@ def __init__(self, player: int, options: Optional[TimespinnerOptions], self.flag_specific_keycards = bool(options and options.specific_keycards) self.flag_eye_spy = bool(options and options.eye_spy) self.flag_unchained_keys = bool(options and options.unchained_keys) + self.flag_prism_break = bool(options and options.prism_break) if precalculated_weights: if self.flag_unchained_keys: @@ -92,6 +93,8 @@ def can_break_walls(self, state: CollectionState) -> bool: return True def can_kill_all_3_bosses(self, state: CollectionState) -> bool: + if self.flag_prism_break: + return state.has_all({'Laser Access M', 'Laser Access I', 'Laser Access A'}, self.player) return state.has_all({'Killed Maw', 'Killed Twins', 'Killed Aelana'}, self.player) def has_teleport(self, state: CollectionState) -> bool: diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index c06dd36797fd..72f2d8b35abf 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -180,12 +180,19 @@ class DamageRandoOverrides(OptionDict): } class HpCap(Range): - "Sets the number that Lunais's HP maxes out at." + """Sets the number that Lunais's HP maxes out at.""" display_name = "HP Cap" range_start = 1 range_end = 999 default = 999 +class AuraCap(Range): + """Sets the maximum Aura Lunais is allowed to have. Level 1 is 80. Djinn Inferno costs 45.""" + display_name = "Aura Cap" + range_start = 45 + range_end = 999 + default = 999 + class LevelCap(Range): """Sets the max level Lunais can achieve.""" display_name = "Level Cap" @@ -359,13 +366,18 @@ class TrapChance(Range): class Traps(OptionList): """List of traps that may be in the item pool to find""" display_name = "Traps Types" - valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } - default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] + valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap", "Throw Stun Trap" } + default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap", "Throw Stun Trap" ] class PresentAccessWithWheelAndSpindle(Toggle): """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" display_name = "Back to the future" +class PrismBreak(Toggle): + """Adds 3 Laser Access items to the item pool to remove the lasers blocking the military hangar area + instead of needing to beat the Golden Idol, Aelana, and The Maw.""" + display_name = "Prism Break" + @dataclass class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): start_with_jewelry_box: StartWithJewelryBox @@ -383,6 +395,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): damage_rando: DamageRando damage_rando_overrides: DamageRandoOverrides hp_cap: HpCap + aura_cap: AuraCap level_cap: LevelCap extra_earrings_xp: ExtraEarringsXP boss_healing: BossHealing @@ -401,6 +414,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): rising_tides_overrides: RisingTidesOverrides unchained_keys: UnchainedKeys back_to_the_future: PresentAccessWithWheelAndSpindle + prism_break: PrismBreak trap_chance: TrapChance traps: Traps diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 72903bd5ffea..a2a5c7ce9c78 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -102,6 +102,7 @@ def fill_slot_data(self) -> Dict[str, object]: "DamageRando": self.options.damage_rando.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value, "HpCap": self.options.hp_cap.value, + "AuraCap": self.options.aura_cap.value, "LevelCap": self.options.level_cap.value, "ExtraEarringsXP": self.options.extra_earrings_xp.value, "BossHealing": self.options.boss_healing.value, @@ -119,6 +120,7 @@ def fill_slot_data(self) -> Dict[str, object]: "RisingTides": self.options.rising_tides.value, "UnchainedKeys": self.options.unchained_keys.value, "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value, + "PrismBreak": self.options.prism_break.value, "Traps": self.options.traps.value, "DeathLink": self.options.death_link.value, "StinkyMaw": True, @@ -224,6 +226,9 @@ def create_item(self, name: str) -> Item: elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ and not self.options.unchained_keys: item.classification = ItemClassification.filler + elif name in {"Laser Access A", "Laser Access I", "Laser Access M"} \ + and not self.options.prism_break: + item.classification = ItemClassification.filler return item @@ -256,6 +261,11 @@ def get_excluded_items(self) -> Set[str]: excluded_items.add('Modern Warp Beacon') excluded_items.add('Mysterious Warp Beacon') + if not self.options.prism_break: + excluded_items.add('Laser Access A') + excluded_items.add('Laser Access I') + excluded_items.add('Laser Access M') + for item in self.multiworld.precollected_items[self.player]: if item.name not in self.item_name_groups['UseItem']: excluded_items.add(item.name) From d7a0f4cb4c540c8c5045982d6e10968386ea7c39 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:49:36 +0100 Subject: [PATCH 034/112] CI: fix naming of windows build action (#4286) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5a2f52f3222..ab94326d8188 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ env: jobs: # build-release-macos: # LF volunteer - build-win-py310: # RCs will still be built and signed by hand + build-win: # RCs will still be built and signed by hand runs-on: windows-latest steps: - uses: actions/checkout@v4 From b5343a36ff4c500905b537bbf297843578b7389e Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 29 Nov 2024 12:17:56 -0800 Subject: [PATCH 035/112] Core: fix settings API for removal of Python 3.8, 3.9 (#4280) * Core: fix settings API for removal of Python 3.8, 3.9 This is fixing 2 problems: - The `World` class has the annotation: `settings: ClassVar[Optional["Group"]]` so `MyWorld.settings` should not raise an exception like it does for some worlds. With the `Optional` there, it looks like it should return `None` for the worlds that don't use it. So that's what I changed it to. - `Group.update` had some code that required `typing.Union` instead of the Python 3.10 `|` for unions. added unit test for this fix added change in Zillion that I used to discover this problem and used it to test the test * fix copy-pasted stuff * tuple instead of set Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- settings.py | 10 ++++++++-- test/general/test_settings.py | 16 ++++++++++++++++ worlds/AutoWorld.py | 5 ++++- worlds/zillion/__init__.py | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 test/general/test_settings.py diff --git a/settings.py b/settings.py index 792770521459..ccd3458003c2 100644 --- a/settings.py +++ b/settings.py @@ -7,6 +7,7 @@ import os.path import shutil import sys +import types import typing import warnings from enum import IntEnum @@ -162,8 +163,13 @@ def update(self, dct: Dict[str, Any]) -> None: else: # assign value, try to upcast to type hint annotation = self.get_type_hints().get(k, None) - candidates = [] if annotation is None else \ - typing.get_args(annotation) if typing.get_origin(annotation) is Union else [annotation] + candidates = ( + [] if annotation is None else ( + typing.get_args(annotation) + if typing.get_origin(annotation) in (Union, types.UnionType) + else [annotation] + ) + ) none_type = type(None) for cls in candidates: assert isinstance(cls, type), f"{self.__class__.__name__}.{k}: type {cls} not supported in settings" diff --git a/test/general/test_settings.py b/test/general/test_settings.py new file mode 100644 index 000000000000..165d7982b5fb --- /dev/null +++ b/test/general/test_settings.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from settings import Group +from worlds.AutoWorld import AutoWorldRegister + + +class TestSettings(TestCase): + def test_settings_can_update(self) -> None: + """ + Test that world settings can update. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=game_name): + if world_type.settings is not None: + assert isinstance(world_type.settings, Group) + world_type.settings.update({}) # a previous bug had a crash in this call to update diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 3c4edc1b0c3b..ded8701d3b61 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -33,7 +33,10 @@ def settings(cls) -> Any: # actual type is defined in World # lazy loading + caching to minimize runtime cost if cls.__settings is None: from settings import get_settings - cls.__settings = get_settings()[cls.settings_key] + try: + cls.__settings = get_settings()[cls.settings_key] + except AttributeError: + return None return cls.__settings def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister: diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index d5e86bb33292..e689f2f55c58 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -47,7 +47,7 @@ class RomStart(str): """ rom_file: RomFile = RomFile(RomFile.copy_to) - rom_start: typing.Union[RomStart, bool] = RomStart("retroarch") + rom_start: RomStart | bool = RomStart("retroarch") class ZillionWebWorld(WebWorld): From 2fb59d39c93a6b5e36efe25d79c9d626b53139fe Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 29 Nov 2024 12:25:01 -0800 Subject: [PATCH 036/112] Zillion: use "new" settings api and cleaning (#3903) * Zillion: use "new" settings api and cleaning * python 3.10 typing update * don't separate assignments of item link players --- worlds/zillion/__init__.py | 83 ++++++++++--------- worlds/zillion/client.py | 69 +++++++-------- worlds/zillion/gen_data.py | 3 +- worlds/zillion/id_maps.py | 29 +++---- worlds/zillion/logic.py | 13 +-- worlds/zillion/options.py | 10 +-- worlds/zillion/patch.py | 24 ++++-- worlds/zillion/region.py | 10 ++- worlds/zillion/test/TestReproducibleRandom.py | 7 +- worlds/zillion/test/__init__.py | 8 +- 10 files changed, 136 insertions(+), 120 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index e689f2f55c58..5a4e2bb48f18 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -3,11 +3,12 @@ import functools import settings import threading -import typing -from typing import Any, Dict, List, Set, Tuple, Optional, Union +from typing import Any, ClassVar import os import logging +from typing_extensions import override + from BaseClasses import ItemClassification, LocationProgressType, \ MultiWorld, Item, CollectionState, Entrance, Tutorial @@ -76,7 +77,7 @@ class ZillionWorld(World): options_dataclass = ZillionOptions options: ZillionOptions # type: ignore - settings: typing.ClassVar[ZillionSettings] # type: ignore + settings: ClassVar[ZillionSettings] # type: ignore # these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486 topology_present = True # indicate if world type has any meaningful layout/pathing @@ -89,14 +90,14 @@ class ZillionWorld(World): class LogStreamInterface: logger: logging.Logger - buffer: List[str] + buffer: list[str] def __init__(self, logger: logging.Logger) -> None: self.logger = logger self.buffer = [] def write(self, msg: str) -> None: - if msg.endswith('\n'): + if msg.endswith("\n"): self.buffer.append(msg[:-1]) self.logger.debug("".join(self.buffer)) self.buffer = [] @@ -108,21 +109,21 @@ def flush(self) -> None: lsi: LogStreamInterface - id_to_zz_item: Optional[Dict[int, ZzItem]] = None + id_to_zz_item: dict[int, ZzItem] | None = None zz_system: System - _item_counts: "Counter[str]" = Counter() + _item_counts: Counter[str] = Counter() """ These are the items counts that will be in the game, which might be different from the item counts the player asked for in options (if the player asked for something invalid). """ - my_locations: List[ZillionLocation] = [] + my_locations: list[ZillionLocation] = [] """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ slot_data_ready: threading.Event """ This event is set in `generate_output` when the data is ready for `fill_slot_data` """ - logic_cache: Union[ZillionLogicCache, None] = None + logic_cache: ZillionLogicCache | None = None - def __init__(self, world: MultiWorld, player: int): + def __init__(self, world: MultiWorld, player: int) -> None: super().__init__(world, player) self.logger = logging.getLogger("Zillion") self.lsi = ZillionWorld.LogStreamInterface(self.logger) @@ -133,6 +134,7 @@ def _make_item_maps(self, start_char: Chars) -> None: _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) self.id_to_zz_item = id_to_zz_item + @override def generate_early(self) -> None: zz_op, item_counts = validate(self.options) @@ -150,12 +152,13 @@ def generate_early(self) -> None: # just in case the options changed anything (I don't think they do) assert self.zz_system.randomizer, "init failed" for zz_name in self.zz_system.randomizer.locations: - if zz_name != 'main': + if zz_name != "main": assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \ f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map" self._make_item_maps(zz_op.start_char) + @override def create_regions(self) -> None: assert self.zz_system.randomizer, "generate_early hasn't been called" assert self.id_to_zz_item, "generate_early hasn't been called" @@ -177,23 +180,23 @@ def create_regions(self) -> None: zz_loc.req.gun = 1 assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0 - start = self.zz_system.randomizer.regions['start'] + start = self.zz_system.randomizer.regions["start"] - all: Dict[str, ZillionRegion] = {} + all_regions: dict[str, ZillionRegion] = {} for here_zz_name, zz_r in self.zz_system.randomizer.regions.items(): here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name) - all[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w) - self.multiworld.regions.append(all[here_name]) + all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w) + self.multiworld.regions.append(all_regions[here_name]) limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126) queue = deque([start]) - done: Set[str] = set() + done: set[str] = set() while len(queue): zz_here = queue.popleft() here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name) if here_name in done: continue - here = all[here_name] + here = all_regions[here_name] for zz_loc in zz_here.locations: # if local gun reqs didn't place "keyword" item @@ -217,15 +220,16 @@ def access_rule_wrapped(zz_loc_local: ZzLocation, self.my_locations.append(loc) for zz_dest in zz_here.connections.keys(): - dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name) - dest = all[dest_name] - exit = Entrance(p, f"{here_name} to {dest_name}", here) - here.exits.append(exit) - exit.connect(dest) + dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name) + dest = all_regions[dest_name] + exit_ = Entrance(p, f"{here_name} to {dest_name}", here) + here.exits.append(exit_) + exit_.connect(dest) queue.append(zz_dest) done.add(here.name) + @override def create_items(self) -> None: if not self.id_to_zz_item: self._make_item_maps("JJ") @@ -249,14 +253,11 @@ def create_items(self) -> None: self.logger.debug(f"Zillion Items: {item_name} 1") self.multiworld.itempool.append(self.create_item(item_name)) - def set_rules(self) -> None: - # logic for this game is in create_regions - pass - + @override def generate_basic(self) -> None: assert self.zz_system.randomizer, "generate_early hasn't been called" # main location name is an alias - main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name] + main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name] self.multiworld.get_location(main_loc_name, self.player)\ .place_locked_item(self.create_item("Win")) @@ -264,22 +265,18 @@ def generate_basic(self) -> None: lambda state: state.has("Win", self.player) @staticmethod - def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: + def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: # noqa: ANN401 # item link pools are about to be created in main # JJ can't be an item link unless all the players share the same start_char # (The reason for this is that the JJ ZillionItem will have a different ZzItem depending # on whether the start char is Apple or Champ, and the logic depends on that ZzItem.) for group in multiworld.groups.values(): - # TODO: remove asserts on group when we can specify which members of TypedDict are optional - assert "game" in group - if group["game"] == "Zillion": - assert "item_pool" in group + if group["game"] == "Zillion" and "item_pool" in group: item_pool = group["item_pool"] to_stay: Chars = "JJ" if "JJ" in item_pool: - assert "players" in group - group_players = group["players"] - players_start_chars: List[Tuple[int, Chars]] = [] + group["players"] = group_players = set(group["players"]) + players_start_chars: list[tuple[int, Chars]] = [] for player in group_players: z_world = multiworld.worlds[player] assert isinstance(z_world, ZillionWorld) @@ -291,17 +288,17 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: elif start_char_counts["Champ"] > start_char_counts["Apple"]: to_stay = "Champ" else: # equal - choices: Tuple[Chars, ...] = ("Apple", "Champ") + choices: tuple[Chars, ...] = ("Apple", "Champ") to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: if sc != to_stay: group_players.remove(p) - assert "world" in group group_world = group["world"] assert isinstance(group_world, ZillionWorld) group_world._make_item_maps(to_stay) + @override def post_fill(self) -> None: """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. This happens before progression balancing, so the items may not be in their final locations yet.""" @@ -317,10 +314,10 @@ def finalize_item_locations(self) -> GenData: assert self.zz_system.randomizer, "generate_early hasn't been called" - # debug_zz_loc_ids: Dict[str, int] = {} + # debug_zz_loc_ids: dict[str, int] = {} empty = zz_items[4] multi_item = empty # a different patcher method differentiates empty from ap multi item - multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) + multi_items: dict[str, tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) for z_loc in self.multiworld.get_locations(self.player): assert isinstance(z_loc, ZillionLocation) # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) @@ -343,7 +340,7 @@ def finalize_item_locations(self) -> GenData: # print(id_) # print("size:", len(debug_zz_loc_ids)) - # debug_loc_to_id: Dict[str, int] = {} + # debug_loc_to_id: dict[str, int] = {} # regions = self.zz_randomizer.regions # for region in regions.values(): # for loc in region.locations: @@ -358,10 +355,11 @@ def finalize_item_locations(self) -> GenData: f"in world {self.player} didn't get an item" ) - game_id = self.multiworld.player_name[self.player].encode() + b'\x00' + self.multiworld.seed_name[-6:].encode() + game_id = self.multiworld.player_name[self.player].encode() + b"\x00" + self.multiworld.seed_name[-6:].encode() return GenData(multi_items, self.zz_system.get_game(), game_id) + @override def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use multiworld.random here. If you need any last-second randomization, use self.random instead.""" @@ -383,6 +381,7 @@ def generate_output(self, output_directory: str) -> None: self.logger.debug(f"Zillion player {self.player} finished generate_output") + @override def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot """Fill in the `slot_data` field in the `Connected` network package. This is a way the generator can give custom data to the client. @@ -400,6 +399,7 @@ def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot # end of ordered Main.py calls + @override def create_item(self, name: str) -> Item: """Create an item for this world type and player. Warning: this may be called with self.multiworld = None, for example by MultiServer""" @@ -420,6 +420,7 @@ def create_item(self, name: str) -> Item: z_item = ZillionItem(name, classification, item_id, self.player, zz_item) return z_item + @override def get_filler_item_name(self) -> str: """Called when the item pool needs to be filled with additional items to match location count.""" return "Empty" diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index 09d0565e1c5e..d629df583a81 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -3,7 +3,7 @@ import io import pkgutil import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast +from typing import Any, ClassVar, Coroutine, Protocol, cast from CommonClient import CommonContext, server_loop, gui_enabled, \ ClientCommandProcessor, logger, get_base_parser @@ -11,6 +11,7 @@ from Utils import async_start import colorama +from typing_extensions import override from zilliandomizer.zri.memory import Memory, RescueInfo from zilliandomizer.zri import events @@ -35,11 +36,11 @@ def _cmd_map(self) -> None: class ToggleCallback(Protocol): - def __call__(self) -> None: ... + def __call__(self) -> object: ... class SetRoomCallback(Protocol): - def __call__(self, rooms: List[List[int]]) -> None: ... + def __call__(self, rooms: list[list[int]]) -> object: ... class ZillionContext(CommonContext): @@ -47,7 +48,7 @@ class ZillionContext(CommonContext): command_processor = ZillionCommandProcessor items_handling = 1 # receive items from other players - known_name: Optional[str] + known_name: str | None """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ from_game: "asyncio.Queue[events.EventFromGame]" @@ -56,11 +57,11 @@ class ZillionContext(CommonContext): """ local checks watched by server """ next_item: int """ index in `items_received` """ - ap_id_to_name: Dict[int, str] - ap_id_to_zz_id: Dict[int, int] + ap_id_to_name: dict[int, str] + ap_id_to_zz_id: dict[int, int] start_char: Chars = "JJ" - rescues: Dict[int, RescueInfo] = {} - loc_mem_to_id: Dict[int, int] = {} + rescues: dict[int, RescueInfo] = {} + loc_mem_to_id: dict[int, int] = {} got_room_info: asyncio.Event """ flag for connected to server """ got_slot_data: asyncio.Event @@ -119,22 +120,22 @@ def reset_game_state(self) -> None: self.finished_game = False self.items_received.clear() - # override - def on_deathlink(self, data: Dict[str, Any]) -> None: + @override + def on_deathlink(self, data: dict[str, Any]) -> None: self.to_game.put_nowait(events.DeathEventToGame()) return super().on_deathlink(data) - # override + @override async def server_auth(self, password_requested: bool = False) -> None: if password_requested and not self.password: await super().server_auth(password_requested) if not self.auth: - logger.info('waiting for connection to game...') + logger.info("waiting for connection to game...") return logger.info("logging in to server...") await self.send_connect() - # override + @override def run_gui(self) -> None: from kvui import GameManager from kivy.core.text import Label as CoreLabel @@ -154,10 +155,10 @@ class MapPanel(Widget): MAP_WIDTH: ClassVar[int] = 281 map_background: CoreImage - _number_textures: List[Texture] = [] - rooms: List[List[int]] = [] + _number_textures: list[Texture] = [] + rooms: list[list[int]] = [] - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 super().__init__(**kwargs) FILE_NAME = "empty-zillion-map-row-col-labels-281.png" @@ -183,7 +184,7 @@ def _make_numbers(self) -> None: label.refresh() self._number_textures.append(label.texture) - def update_map(self, *args: Any) -> None: + def update_map(self, *args: Any) -> None: # noqa: ANN401 self.canvas.clear() with self.canvas: @@ -203,6 +204,7 @@ def update_map(self, *args: Any) -> None: num_texture = self._number_textures[num] Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + @override def build(self) -> Layout: container = super().build() self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=ZillionManager.MapPanel.MAP_WIDTH) @@ -216,17 +218,18 @@ def toggle_map_width(self) -> None: self.map_widget.width = 0 self.container.do_layout() - def set_rooms(self, rooms: List[List[int]]) -> None: + def set_rooms(self, rooms: list[list[int]]) -> None: self.map_widget.rooms = rooms self.map_widget.update_map() self.ui = ZillionManager(self) - self.ui_toggle_map = lambda: self.ui.toggle_map_width() - self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + self.ui_toggle_map = lambda: isinstance(self.ui, ZillionManager) and self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: isinstance(self.ui, ZillionManager) and self.ui.set_rooms(rooms) run_co: Coroutine[Any, Any, None] = self.ui.async_run() self.ui_task = asyncio.create_task(run_co, name="UI") - def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + @override + def on_package(self, cmd: str, args: dict[str, Any]) -> None: self.room_item_numbers_to_ui() if cmd == "Connected": logger.info("logged in to Archipelago server") @@ -238,7 +241,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None: if "start_char" not in slot_data: logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") return - self.start_char = slot_data['start_char'] + self.start_char = slot_data["start_char"] if self.start_char not in {"Apple", "Champ", "JJ"}: logger.warning("invalid Zillion `Connected` packet, " f"`slot_data` `start_char` has invalid value: {self.start_char}") @@ -259,7 +262,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None: self.rescues[0 if rescue_id == "0" else 1] = ri if "loc_mem_to_id" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") return loc_mem_to_id = slot_data["loc_mem_to_id"] self.loc_mem_to_id = {} @@ -286,7 +289,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None: if "keys" not in args: logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") return - keys = cast(Dict[str, Optional[str]], args["keys"]) + keys = cast(dict[str, str | None], args["keys"]) doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) if doors_b64: logger.info("received door data from server") @@ -321,9 +324,9 @@ def process_from_game_queue(self) -> None: if server_id in self.missing_locations: self.ap_local_count += 1 n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win - logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + logger.info(f"New Check: {loc_name} ({self.ap_local_count}/{n_locations})") async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [server_id]} + {"cmd": "LocationChecks", "locations": [server_id]} ])) else: # This will happen a lot in Zillion, @@ -334,7 +337,7 @@ def process_from_game_queue(self) -> None: elif isinstance(event_from_game, events.WinEventFromGame): if not self.finished_game: async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]}, + {"cmd": "LocationChecks", "locations": [loc_name_to_id["J-6 bottom far left"]]}, {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} ])) self.finished_game = True @@ -362,24 +365,24 @@ def process_items_received(self) -> None: ap_id = self.items_received[index].item from_name = self.player_names[self.items_received[index].player] # TODO: colors in this text, like sni client? - logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + logger.info(f"received {self.ap_id_to_name[ap_id]} from {from_name}") self.to_game.put_nowait( events.ItemEventToGame(zz_item_ids) ) self.next_item = len(self.items_received) -def name_seed_from_ram(data: bytes) -> Tuple[str, str]: +def name_seed_from_ram(data: bytes) -> tuple[str, str]: """ returns player name, and end of seed string """ if len(data) == 0: # no connection to game return "", "xxx" - null_index = data.find(b'\x00') + null_index = data.find(b"\x00") if null_index == -1: logger.warning(f"invalid game id in rom {repr(data)}") null_index = len(data) name = data[:null_index].decode() - null_index_2 = data.find(b'\x00', null_index + 1) + null_index_2 = data.find(b"\x00", null_index + 1) if null_index_2 == -1: null_index_2 = len(data) seed_name = data[null_index + 1:null_index_2].decode() @@ -479,8 +482,8 @@ def log_no_spam(msg: str) -> None: async def main() -> None: parser = get_base_parser() - parser.add_argument('diff_file', default="", type=str, nargs="?", - help='Path to a .apzl Archipelago Binary Patch file') + parser.add_argument("diff_file", default="", type=str, nargs="?", + help="Path to a .apzl Archipelago Binary Patch file") # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) args = parser.parse_args() print(args) diff --git a/worlds/zillion/gen_data.py b/worlds/zillion/gen_data.py index 13cbee9ced20..214073396153 100644 --- a/worlds/zillion/gen_data.py +++ b/worlds/zillion/gen_data.py @@ -1,6 +1,5 @@ from dataclasses import dataclass import json -from typing import Dict, Tuple from zilliandomizer.game import Game as ZzGame @@ -9,7 +8,7 @@ class GenData: """ data passed from generation to patcher """ - multi_items: Dict[str, Tuple[str, str]] + multi_items: dict[str, tuple[str, str]] """ zz_loc_name to (item_name, player_name) """ zz_game: ZzGame game_id: bytes diff --git a/worlds/zillion/id_maps.py b/worlds/zillion/id_maps.py index 32d71fc79b30..25762f99cd6b 100644 --- a/worlds/zillion/id_maps.py +++ b/worlds/zillion/id_maps.py @@ -1,5 +1,6 @@ from collections import defaultdict -from typing import Dict, Iterable, Mapping, Tuple, TypedDict +from collections.abc import Iterable, Mapping +from typing import TypedDict from zilliandomizer.logic_components.items import ( Item as ZzItem, @@ -40,13 +41,13 @@ _zz_empty = zz_item_name_to_zz_item["empty"] -def make_id_to_others(start_char: Chars) -> Tuple[ - Dict[int, str], Dict[int, int], Dict[int, ZzItem] +def make_id_to_others(start_char: Chars) -> tuple[ + dict[int, str], dict[int, int], dict[int, ZzItem] ]: """ returns id_to_name, id_to_zz_id, id_to_zz_item """ - id_to_name: Dict[int, str] = {} - id_to_zz_id: Dict[int, int] = {} - id_to_zz_item: Dict[int, ZzItem] = {} + id_to_name: dict[int, str] = {} + id_to_zz_id: dict[int, int] = {} + id_to_zz_item: dict[int, ZzItem] = {} if start_char == "JJ": name_to_zz_item = { @@ -91,14 +92,14 @@ def make_room_name(row: int, col: int) -> str: return f"{chr(ord('A') + row - 1)}-{col + 1}" -loc_name_to_id: Dict[str, int] = { +loc_name_to_id: dict[str, int] = { name: id_ + base_id for name, id_ in pretty_loc_name_to_id.items() } def zz_reg_name_to_reg_name(zz_reg_name: str) -> str: - if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c': + if zz_reg_name[0] == "r" and zz_reg_name[3] == "c": row, col = parse_reg_name(zz_reg_name) end = zz_reg_name[5:] return f"{make_room_name(row, col)} {end.upper()}" @@ -113,17 +114,17 @@ class ClientRescue(TypedDict): class ZillionSlotInfo(TypedDict): start_char: Chars - rescues: Dict[str, ClientRescue] - loc_mem_to_id: Dict[int, int] + rescues: dict[str, ClientRescue] + loc_mem_to_id: dict[int, int] """ memory location of canister to Archipelago location id number """ def get_slot_info(regions: Iterable[RegionData], start_char: Chars, loc_name_to_pretty: Mapping[str, str]) -> ZillionSlotInfo: - items_placed_in_map_index: Dict[int, int] = defaultdict(int) - rescue_locations: Dict[int, RescueInfo] = {} - loc_memory_to_loc_id: Dict[int, int] = {} + items_placed_in_map_index: dict[int, int] = defaultdict(int) + rescue_locations: dict[int, RescueInfo] = {} + loc_memory_to_loc_id: dict[int, int] = {} for region in regions: for loc in region.locations: assert loc.item, ("There should be an item placed in every location before " @@ -142,7 +143,7 @@ def get_slot_info(regions: Iterable[RegionData], loc_memory_to_loc_id[loc_memory] = pretty_loc_name_to_id[loc_name_to_pretty[loc.name]] items_placed_in_map_index[map_index] += 1 - rescues: Dict[str, ClientRescue] = {} + rescues: dict[str, ClientRescue] = {} for i in (0, 1): if i in rescue_locations: ri = rescue_locations[i] diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index a14910a200e5..f3d1814a9e9b 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,4 +1,5 @@ -from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter +from collections import Counter +from collections.abc import Mapping from BaseClasses import CollectionState @@ -35,7 +36,7 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: return _hash -def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: +def item_counts(cs: CollectionState, p: int) -> tuple[tuple[str, int], ...]: """ the zilliandomizer items that player p has collected @@ -44,11 +45,11 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) -_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset()) +_cache_miss: tuple[None, frozenset[Location]] = (None, frozenset()) class ZillionLogicCache: - _cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]] + _cache: dict[int, tuple[Counter[str], frozenset[Location]]] """ `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """ _player: int _zz_r: Randomizer @@ -60,7 +61,7 @@ def __init__(self, player: int, zz_r: Randomizer, id_to_zz_item: Mapping[int, It self._zz_r = zz_r self._id_to_zz_item = id_to_zz_item - def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]: + def cs_to_zz_locs(self, cs: CollectionState) -> frozenset[Location]: """ given an Archipelago `CollectionState`, returns frozenset of accessible zilliandomizer locations @@ -76,7 +77,7 @@ def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]: return locs # print("cache miss") - have_items: List[Item] = [] + have_items: list[Item] = [] for name, count in counts: have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count) # have_req is the result of converting AP CollectionState to zilliandomizer collection state diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index ec0fdb0b22e1..22a698472265 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,6 +1,6 @@ from collections import Counter from dataclasses import dataclass -from typing import ClassVar, Dict, Literal, Tuple, TypeGuard +from typing import ClassVar, Literal, TypeGuard from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle @@ -107,7 +107,7 @@ class ZillionStartChar(Choice): display_name = "start character" default = "random" - _name_capitalization: ClassVar[Dict[int, Chars]] = { + _name_capitalization: ClassVar[dict[int, Chars]] = { option_jj: "JJ", option_apple: "Apple", option_champ: "Champ", @@ -263,7 +263,7 @@ class ZillionMapGen(Choice): option_full = 2 default = 0 - def zz_value(self) -> Literal['none', 'rooms', 'full']: + def zz_value(self) -> Literal["none", "rooms", "full"]: if self.value == ZillionMapGen.option_none: return "none" if self.value == ZillionMapGen.option_rooms: @@ -305,7 +305,7 @@ class ZillionOptions(PerGameCommonOptions): ] -def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: +def convert_item_counts(ic: Counter[str]) -> ZzItemCounts: tr: ZzItemCounts = { ID.card: ic["ID Card"], ID.red: ic["Red ID Card"], @@ -319,7 +319,7 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions) -> tuple[ZzOptions, Counter[str]]: """ adjusts options to make game completion possible diff --git a/worlds/zillion/patch.py b/worlds/zillion/patch.py index 6bc6d04dd663..0eee3315f4a1 100644 --- a/worlds/zillion/patch.py +++ b/worlds/zillion/patch.py @@ -1,5 +1,5 @@ import os -from typing import Any, BinaryIO, Optional, cast +from typing import BinaryIO import zipfile from typing_extensions import override @@ -11,11 +11,11 @@ from .gen_data import GenData -USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270' +US_HASH = "d4bf9e7bcf9a48da53785d2ae7bc4270" class ZillionPatch(APAutoPatchInterface): - hash = USHASH + hash = US_HASH game = "Zillion" patch_file_ending = ".apzl" result_file_ending = ".sms" @@ -23,8 +23,14 @@ class ZillionPatch(APAutoPatchInterface): gen_data_str: str """ JSON encoded """ - def __init__(self, *args: Any, gen_data_str: str = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + def __init__(self, + path: str | None = None, + player: int | None = None, + player_name: str = "", + server: str = "", + *, + gen_data_str: str = "") -> None: + super().__init__(path=path, player=player, player_name=player_name, server=server) self.gen_data_str = gen_data_str @classmethod @@ -44,15 +50,17 @@ def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: super().read_contents(opened_zipfile) self.gen_data_str = opened_zipfile.read("gen_data.json").decode() + @override def patch(self, target: str) -> None: self.read() write_rom_from_gen_data(self.gen_data_str, target) -def get_base_rom_path(file_name: Optional[str] = None) -> str: - options = Utils.get_options() +def get_base_rom_path(file_name: str | None = None) -> str: + from . import ZillionSettings, ZillionWorld + settings: ZillionSettings = ZillionWorld.settings if not file_name: - file_name = cast(str, options["zillion_options"]["rom_file"]) + file_name = settings.rom_file if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/zillion/region.py b/worlds/zillion/region.py index cf5aa6588950..40565f008263 100644 --- a/worlds/zillion/region.py +++ b/worlds/zillion/region.py @@ -1,9 +1,11 @@ -from typing import Optional -from BaseClasses import MultiWorld, Region, Location, Item, CollectionState +from typing_extensions import override + from zilliandomizer.logic_components.regions import Region as ZzRegion from zilliandomizer.logic_components.locations import Location as ZzLocation from zilliandomizer.logic_components.items import RESCUE +from BaseClasses import MultiWorld, Region, Location, Item, CollectionState + from .id_maps import loc_name_to_id from .item import ZillionItem @@ -28,12 +30,12 @@ def __init__(self, zz_loc: ZzLocation, player: int, name: str, - parent: Optional[Region] = None) -> None: + parent: Region | None = None) -> None: loc_id = loc_name_to_id[name] super().__init__(player, name, loc_id, parent) self.zz_loc = zz_loc - # override + @override def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: saved_gun_req = -1 if isinstance(item, ZillionItem) \ diff --git a/worlds/zillion/test/TestReproducibleRandom.py b/worlds/zillion/test/TestReproducibleRandom.py index a92fae240709..352165449a8b 100644 --- a/worlds/zillion/test/TestReproducibleRandom.py +++ b/worlds/zillion/test/TestReproducibleRandom.py @@ -1,4 +1,3 @@ -from typing import cast from . import ZillionTestBase from .. import ZillionWorld @@ -9,7 +8,8 @@ class SeedTest(ZillionTestBase): def test_reproduce_seed(self) -> None: self.world_setup(42) - z_world = cast(ZillionWorld, self.multiworld.worlds[1]) + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) r = z_world.zz_system.randomizer assert r randomized_requirements_first = tuple( @@ -18,7 +18,8 @@ def test_reproduce_seed(self) -> None: ) self.world_setup(42) - z_world = cast(ZillionWorld, self.multiworld.worlds[1]) + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) r = z_world.zz_system.randomizer assert r randomized_requirements_second = tuple( diff --git a/worlds/zillion/test/__init__.py b/worlds/zillion/test/__init__.py index fe62bae34c9e..a669442364fe 100644 --- a/worlds/zillion/test/__init__.py +++ b/worlds/zillion/test/__init__.py @@ -1,4 +1,3 @@ -from typing import cast from test.bases import WorldTestBase from .. import ZillionWorld @@ -13,8 +12,9 @@ def ensure_gun_3_requirement(self) -> None: This makes sure that gun 3 is required by making all the canisters in O-7 (including key word canisters) require gun 3. """ - zz_world = cast(ZillionWorld, self.multiworld.worlds[1]) - assert zz_world.zz_system.randomizer - for zz_loc_name, zz_loc in zz_world.zz_system.randomizer.locations.items(): + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) + assert z_world.zz_system.randomizer + for zz_loc_name, zz_loc in z_world.zz_system.randomizer.locations.items(): if zz_loc_name.startswith("r15c6"): zz_loc.req.gun = 3 From 8444ffa0c7cf8dbc404d7197204f4810d0a54214 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:34:14 -0500 Subject: [PATCH 037/112] id Tech: Standardizing and fixing display names (#4240) --- worlds/doom_1993/Options.py | 2 +- worlds/doom_ii/Options.py | 2 +- worlds/heretic/Options.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/doom_1993/Options.py b/worlds/doom_1993/Options.py index f65952d3eb49..b4fc50aac674 100644 --- a/worlds/doom_1993/Options.py +++ b/worlds/doom_1993/Options.py @@ -112,7 +112,7 @@ class StartWithComputerAreaMaps(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_name="Reset Level on Death" + display_name = "Reset Level on Death" class Episode1(DefaultOnToggle): diff --git a/worlds/doom_ii/Options.py b/worlds/doom_ii/Options.py index cc39512a176e..6ecfb1aecd6c 100644 --- a/worlds/doom_ii/Options.py +++ b/worlds/doom_ii/Options.py @@ -102,7 +102,7 @@ class StartWithComputerAreaMaps(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_message="Reset level on death" + display_name = "Reset Level on Death" class Episode1(DefaultOnToggle): diff --git a/worlds/heretic/Options.py b/worlds/heretic/Options.py index 75e2257a7336..14f4198a55f0 100644 --- a/worlds/heretic/Options.py +++ b/worlds/heretic/Options.py @@ -104,7 +104,7 @@ class StartWithMapScrolls(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_message="Reset level on death" + display_name = "Reset Level on Death" class CheckSanity(Toggle): From c97e4866ddfd8eb31bdd3f22b1b63d1b91c62e03 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:43:01 +0100 Subject: [PATCH 038/112] Core: Rewrite start inventory from pool code (#3778) * Rewrite start inventory from pool code * I think this is nicer? * lol * I just made it even shorter and nicer * comments :D * I think this makes more logical sense * final change I promise * HOLD UP THIS IS SO SHORT NOW * ???????? Vi pls * ???????? Vi pls???????????????? * this was probably important idk * Lmao this just did not work correctly at all --- Main.py | 65 +++++++++++++++++++++++++-------------------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/Main.py b/Main.py index 6b94b84c278b..606514630f0d 100644 --- a/Main.py +++ b/Main.py @@ -153,45 +153,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # remove starting inventory from pool items. # Because some worlds don't actually create items during create_items this has to be as late as possible. - if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): - new_items: List[Item] = [] - old_items: List[Item] = [] - depletion_pool: Dict[int, Dict[str, int]] = { - player: getattr(multiworld.worlds[player].options, - "start_inventory_from_pool", - StartInventoryPool({})).value.copy() - for player in multiworld.player_ids - } - for player, items in depletion_pool.items(): - player_world: AutoWorld.World = multiworld.worlds[player] - for count in items.values(): - for _ in range(count): - new_items.append(player_world.create_filler()) - target: int = sum(sum(items.values()) for items in depletion_pool.values()) - for i, item in enumerate(multiworld.itempool): + fallback_inventory = StartInventoryPool({}) + depletion_pool: Dict[int, Dict[str, int]] = { + player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy() + for player in multiworld.player_ids + } + target_per_player = { + player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items + } + + if target_per_player: + new_itempool: List[Item] = [] + + # Make new itempool with start_inventory_from_pool items removed + for item in multiworld.itempool: if depletion_pool[item.player].get(item.name, 0): - target -= 1 depletion_pool[item.player][item.name] -= 1 - # quick abort if we have found all items - if not target: - old_items.extend(multiworld.itempool[i+1:]) - break else: - old_items.append(item) - - # leftovers? - if target: - for player, remaining_items in depletion_pool.items(): - remaining_items = {name: count for name, count in remaining_items.items() if count} - if remaining_items: - logger.warning(f"{multiworld.get_player_name(player)}" - f" is trying to remove items from their pool that don't exist: {remaining_items}") - # find all filler we generated for the current player and remove until it matches - removables = [item for item in new_items if item.player == player] - for _ in range(sum(remaining_items.values())): - new_items.remove(removables.pop()) - assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change." - multiworld.itempool[:] = new_items + old_items + new_itempool.append(item) + + # Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool + for player, target in target_per_player.items(): + unfound_items = {item: count for item, count in depletion_pool[player].items() if count} + + if unfound_items: + player_name = multiworld.get_player_name(player) + logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}") + + needed_items = target_per_player[player] - sum(unfound_items.values()) + new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)] + + assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change." + multiworld.itempool[:] = new_itempool multiworld.link_items() From 6e5adc7abdecf4baf6a29d89934cfe6cea0cba82 Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Fri, 29 Nov 2024 16:45:36 -0500 Subject: [PATCH 039/112] New Game: Faxanadu (#3059) --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/faxanadu/Items.py | 58 ++++++++ worlds/faxanadu/Locations.py | 199 ++++++++++++++++++++++++++++ worlds/faxanadu/Options.py | 107 +++++++++++++++ worlds/faxanadu/Regions.py | 66 +++++++++ worlds/faxanadu/Rules.py | 79 +++++++++++ worlds/faxanadu/__init__.py | 190 ++++++++++++++++++++++++++ worlds/faxanadu/docs/en_Faxanadu.md | 27 ++++ worlds/faxanadu/docs/setup_en.md | 32 +++++ 10 files changed, 762 insertions(+) create mode 100644 worlds/faxanadu/Items.py create mode 100644 worlds/faxanadu/Locations.py create mode 100644 worlds/faxanadu/Options.py create mode 100644 worlds/faxanadu/Regions.py create mode 100644 worlds/faxanadu/Rules.py create mode 100644 worlds/faxanadu/__init__.py create mode 100644 worlds/faxanadu/docs/en_Faxanadu.md create mode 100644 worlds/faxanadu/docs/setup_en.md diff --git a/README.md b/README.md index 0e57bce53b51..2cc3c18aa09d 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Currently, the following games are supported: * Kingdom Hearts 1 * Mega Man 2 * Yacht Dice +* Faxanadu 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 diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index a51cac37026b..4b7496c686c8 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -63,6 +63,9 @@ # Factorio /worlds/factorio/ @Berserker66 +# Faxanadu +/worlds/faxanadu/ @Daivuk + # Final Fantasy Mystic Quest /worlds/ffmq/ @Alchav @wildham0 diff --git a/worlds/faxanadu/Items.py b/worlds/faxanadu/Items.py new file mode 100644 index 000000000000..4815fde9de66 --- /dev/null +++ b/worlds/faxanadu/Items.py @@ -0,0 +1,58 @@ +from BaseClasses import ItemClassification +from typing import List, Optional + + +class ItemDef: + def __init__(self, + id: Optional[int], + name: str, + classification: ItemClassification, + count: int, + progression_count: int, + prefill_location: Optional[str]): + self.id = id + self.name = name + self.classification = classification + self.count = count + self.progression_count = progression_count + self.prefill_location = prefill_location + + +items: List[ItemDef] = [ + ItemDef(400000, 'Progressive Sword', ItemClassification.progression, 4, 0, None), + ItemDef(400001, 'Progressive Armor', ItemClassification.progression, 3, 0, None), + ItemDef(400002, 'Progressive Shield', ItemClassification.useful, 4, 0, None), + ItemDef(400003, 'Spring Elixir', ItemClassification.progression, 1, 0, None), + ItemDef(400004, 'Mattock', ItemClassification.progression, 1, 0, None), + ItemDef(400005, 'Unlock Wingboots', ItemClassification.progression, 1, 0, None), + ItemDef(400006, 'Key Jack', ItemClassification.progression, 1, 0, None), + ItemDef(400007, 'Key Queen', ItemClassification.progression, 1, 0, None), + ItemDef(400008, 'Key King', ItemClassification.progression, 1, 0, None), + ItemDef(400009, 'Key Joker', ItemClassification.progression, 1, 0, None), + ItemDef(400010, 'Key Ace', ItemClassification.progression, 1, 0, None), + ItemDef(400011, 'Ring of Ruby', ItemClassification.progression, 1, 0, None), + ItemDef(400012, 'Ring of Dworf', ItemClassification.progression, 1, 0, None), + ItemDef(400013, 'Demons Ring', ItemClassification.progression, 1, 0, None), + ItemDef(400014, 'Black Onyx', ItemClassification.progression, 1, 0, None), + ItemDef(None, 'Sky Spring Flow', ItemClassification.progression, 1, 0, 'Sky Spring'), + ItemDef(None, 'Tower of Fortress Spring Flow', ItemClassification.progression, 1, 0, 'Tower of Fortress Spring'), + ItemDef(None, 'Joker Spring Flow', ItemClassification.progression, 1, 0, 'Joker Spring'), + ItemDef(400015, 'Deluge', ItemClassification.progression, 1, 0, None), + ItemDef(400016, 'Thunder', ItemClassification.useful, 1, 0, None), + ItemDef(400017, 'Fire', ItemClassification.useful, 1, 0, None), + ItemDef(400018, 'Death', ItemClassification.useful, 1, 0, None), + ItemDef(400019, 'Tilte', ItemClassification.useful, 1, 0, None), + ItemDef(400020, 'Ring of Elf', ItemClassification.useful, 1, 0, None), + ItemDef(400021, 'Magical Rod', ItemClassification.useful, 1, 0, None), + ItemDef(400022, 'Pendant', ItemClassification.useful, 1, 0, None), + ItemDef(400023, 'Hourglass', ItemClassification.filler, 6, 0, None), + # We need at least 4 red potions for the Tower of Red Potion. Up to the player to save them up! + ItemDef(400024, 'Red Potion', ItemClassification.filler, 15, 4, None), + ItemDef(400025, 'Elixir', ItemClassification.filler, 4, 0, None), + ItemDef(400026, 'Glove', ItemClassification.filler, 5, 0, None), + ItemDef(400027, 'Ointment', ItemClassification.filler, 8, 0, None), + ItemDef(400028, 'Poison', ItemClassification.trap, 13, 0, None), + ItemDef(None, 'Killed Evil One', ItemClassification.progression, 1, 0, 'Evil One'), + # Placeholder item so the game knows which shop slot to prefill wingboots + ItemDef(400029, 'Wingboots', ItemClassification.useful, 0, 0, None), +] diff --git a/worlds/faxanadu/Locations.py b/worlds/faxanadu/Locations.py new file mode 100644 index 000000000000..ebb785f9391a --- /dev/null +++ b/worlds/faxanadu/Locations.py @@ -0,0 +1,199 @@ +from typing import List, Optional + + +class LocationType(): + world = 1 # Just standing there in the world + hidden = 2 # Kill all monsters in the room to reveal, each "item room" counter tick. + boss_reward = 3 # Kill a boss to reveal the item + shop = 4 # Buy at a shop + give = 5 # Given by an NPC + spring = 6 # Activatable spring + boss = 7 # Entity to kill to trigger the check + + +class ItemType(): + unknown = 0 # Or don't care + red_potion = 1 + + +class LocationDef: + def __init__(self, id: Optional[int], name: str, region: str, type: int, original_item: int): + self.id = id + self.name = name + self.region = region + self.type = type + self.original_item = original_item + + +locations: List[LocationDef] = [ + # Eolis + LocationDef(400100, 'Eolis Guru', 'Eolis', LocationType.give, ItemType.unknown), + LocationDef(400101, 'Eolis Key Jack', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400102, 'Eolis Hand Dagger', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400103, 'Eolis Red Potion', 'Eolis', LocationType.shop, ItemType.red_potion), + LocationDef(400104, 'Eolis Elixir', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400105, 'Eolis Deluge', 'Eolis', LocationType.shop, ItemType.unknown), + + # Path to Apolune + LocationDef(400106, 'Path to Apolune Magic Shield', 'Path to Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400107, 'Path to Apolune Death', 'Path to Apolune', LocationType.shop, ItemType.unknown), + + # Apolune + LocationDef(400108, 'Apolune Small Shield', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400109, 'Apolune Hand Dagger', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400110, 'Apolune Deluge', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400111, 'Apolune Red Potion', 'Apolune', LocationType.shop, ItemType.red_potion), + LocationDef(400112, 'Apolune Key Jack', 'Apolune', LocationType.shop, ItemType.unknown), + + # Tower of Trunk + LocationDef(400113, 'Tower of Trunk Hidden Mattock', 'Tower of Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400114, 'Tower of Trunk Hidden Hourglass', 'Tower of Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400115, 'Tower of Trunk Boss Mattock', 'Tower of Trunk', LocationType.boss_reward, ItemType.unknown), + + # Path to Forepaw + LocationDef(400116, 'Path to Forepaw Hidden Red Potion', 'Path to Forepaw', LocationType.hidden, ItemType.red_potion), + LocationDef(400117, 'Path to Forepaw Glove', 'Path to Forepaw', LocationType.world, ItemType.unknown), + + # Forepaw + LocationDef(400118, 'Forepaw Long Sword', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400119, 'Forepaw Studded Mail', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400120, 'Forepaw Small Shield', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400121, 'Forepaw Red Potion', 'Forepaw', LocationType.shop, ItemType.red_potion), + LocationDef(400122, 'Forepaw Wingboots', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400123, 'Forepaw Key Jack', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400124, 'Forepaw Key Queen', 'Forepaw', LocationType.shop, ItemType.unknown), + + # Trunk + LocationDef(400125, 'Trunk Hidden Ointment', 'Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400126, 'Trunk Hidden Red Potion', 'Trunk', LocationType.hidden, ItemType.red_potion), + LocationDef(400127, 'Trunk Red Potion', 'Trunk', LocationType.world, ItemType.red_potion), + LocationDef(None, 'Sky Spring', 'Trunk', LocationType.spring, ItemType.unknown), + + # Joker Spring + LocationDef(400128, 'Joker Spring Ruby Ring', 'Joker Spring', LocationType.give, ItemType.unknown), + LocationDef(None, 'Joker Spring', 'Joker Spring', LocationType.spring, ItemType.unknown), + + # Tower of Fortress + LocationDef(400129, 'Tower of Fortress Poison 1', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400130, 'Tower of Fortress Poison 2', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400131, 'Tower of Fortress Hidden Wingboots', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400132, 'Tower of Fortress Ointment', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400133, 'Tower of Fortress Boss Wingboots', 'Tower of Fortress', LocationType.boss_reward, ItemType.unknown), + LocationDef(400134, 'Tower of Fortress Elixir', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400135, 'Tower of Fortress Guru', 'Tower of Fortress', LocationType.give, ItemType.unknown), + LocationDef(None, 'Tower of Fortress Spring', 'Tower of Fortress', LocationType.spring, ItemType.unknown), + + # Path to Mascon + LocationDef(400136, 'Path to Mascon Hidden Wingboots', 'Path to Mascon', LocationType.hidden, ItemType.unknown), + + # Tower of Red Potion + LocationDef(400137, 'Tower of Red Potion', 'Tower of Red Potion', LocationType.world, ItemType.red_potion), + + # Mascon + LocationDef(400138, 'Mascon Large Shield', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400139, 'Mascon Thunder', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400140, 'Mascon Mattock', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400141, 'Mascon Red Potion', 'Mascon', LocationType.shop, ItemType.red_potion), + LocationDef(400142, 'Mascon Key Jack', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400143, 'Mascon Key Queen', 'Mascon', LocationType.shop, ItemType.unknown), + + # Path to Victim + LocationDef(400144, 'Misty Shop Death', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400145, 'Misty Shop Hourglass', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400146, 'Misty Shop Elixir', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400147, 'Misty Shop Red Potion', 'Path to Victim', LocationType.shop, ItemType.red_potion), + LocationDef(400148, 'Misty Doctor Office', 'Path to Victim', LocationType.hidden, ItemType.unknown), + + # Tower of Suffer + LocationDef(400149, 'Tower of Suffer Hidden Wingboots', 'Tower of Suffer', LocationType.hidden, ItemType.unknown), + LocationDef(400150, 'Tower of Suffer Hidden Hourglass', 'Tower of Suffer', LocationType.hidden, ItemType.unknown), + LocationDef(400151, 'Tower of Suffer Pendant', 'Tower of Suffer', LocationType.boss_reward, ItemType.unknown), + + # Victim + LocationDef(400152, 'Victim Full Plate', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400153, 'Victim Mattock', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400154, 'Victim Red Potion', 'Victim', LocationType.shop, ItemType.red_potion), + LocationDef(400155, 'Victim Key King', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400156, 'Victim Key Queen', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400157, 'Victim Tavern', 'Mist', LocationType.give, ItemType.unknown), + + # Mist + LocationDef(400158, 'Mist Hidden Poison 1', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400159, 'Mist Hidden Poison 2', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400160, 'Mist Hidden Wingboots', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400161, 'Misty Magic Hall', 'Mist', LocationType.give, ItemType.unknown), + LocationDef(400162, 'Misty House', 'Mist', LocationType.give, ItemType.unknown), + + # Useless Tower + LocationDef(400163, 'Useless Tower', 'Useless Tower', LocationType.hidden, ItemType.unknown), + + # Tower of Mist + LocationDef(400164, 'Tower of Mist Hidden Ointment', 'Tower of Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400165, 'Tower of Mist Elixir', 'Tower of Mist', LocationType.world, ItemType.unknown), + LocationDef(400166, 'Tower of Mist Black Onyx', 'Tower of Mist', LocationType.boss_reward, ItemType.unknown), + + # Path to Conflate + LocationDef(400167, 'Path to Conflate Hidden Ointment', 'Path to Conflate', LocationType.hidden, ItemType.unknown), + LocationDef(400168, 'Path to Conflate Poison', 'Path to Conflate', LocationType.hidden, ItemType.unknown), + + # Helm Branch + LocationDef(400169, 'Helm Branch Hidden Glove', 'Helm Branch', LocationType.hidden, ItemType.unknown), + LocationDef(400170, 'Helm Branch Battle Helmet', 'Helm Branch', LocationType.boss_reward, ItemType.unknown), + + # Conflate + LocationDef(400171, 'Conflate Giant Blade', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400172, 'Conflate Magic Shield', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400173, 'Conflate Wingboots', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400174, 'Conflate Red Potion', 'Conflate', LocationType.shop, ItemType.red_potion), + LocationDef(400175, 'Conflate Guru', 'Conflate', LocationType.give, ItemType.unknown), + + # Branches + LocationDef(400176, 'Branches Hidden Ointment', 'Branches', LocationType.hidden, ItemType.unknown), + LocationDef(400177, 'Branches Poison', 'Branches', LocationType.world, ItemType.unknown), + LocationDef(400178, 'Branches Hidden Mattock', 'Branches', LocationType.hidden, ItemType.unknown), + LocationDef(400179, 'Branches Hidden Hourglass', 'Branches', LocationType.hidden, ItemType.unknown), + + # Path to Daybreak + LocationDef(400180, 'Path to Daybreak Hidden Wingboots 1', 'Path to Daybreak', LocationType.hidden, ItemType.unknown), + LocationDef(400181, 'Path to Daybreak Magical Rod', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400182, 'Path to Daybreak Hidden Wingboots 2', 'Path to Daybreak', LocationType.hidden, ItemType.unknown), + LocationDef(400183, 'Path to Daybreak Poison', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400184, 'Path to Daybreak Glove', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400185, 'Path to Daybreak Battle Suit', 'Path to Daybreak', LocationType.boss_reward, ItemType.unknown), + + # Daybreak + LocationDef(400186, 'Daybreak Tilte', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400187, 'Daybreak Giant Blade', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400188, 'Daybreak Red Potion', 'Daybreak', LocationType.shop, ItemType.red_potion), + LocationDef(400189, 'Daybreak Key King', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400190, 'Daybreak Key Queen', 'Daybreak', LocationType.shop, ItemType.unknown), + + # Dartmoor Castle + LocationDef(400191, 'Dartmoor Castle Hidden Hourglass', 'Dartmoor Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400192, 'Dartmoor Castle Hidden Red Potion', 'Dartmoor Castle', LocationType.hidden, ItemType.red_potion), + + # Dartmoor + LocationDef(400193, 'Dartmoor Giant Blade', 'Dartmoor', LocationType.shop, ItemType.unknown), + LocationDef(400194, 'Dartmoor Red Potion', 'Dartmoor', LocationType.shop, ItemType.red_potion), + LocationDef(400195, 'Dartmoor Key King', 'Dartmoor', LocationType.shop, ItemType.unknown), + + # Fraternal Castle + LocationDef(400196, 'Fraternal Castle Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400197, 'Fraternal Castle Shop Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400198, 'Fraternal Castle Poison 1', 'Fraternal Castle', LocationType.world, ItemType.unknown), + LocationDef(400199, 'Fraternal Castle Poison 2', 'Fraternal Castle', LocationType.world, ItemType.unknown), + LocationDef(400200, 'Fraternal Castle Poison 3', 'Fraternal Castle', LocationType.world, ItemType.unknown), + # LocationDef(400201, 'Fraternal Castle Red Potion', 'Fraternal Castle', LocationType.world, ItemType.red_potion), # This location is inaccessible. Keeping commented for context. + LocationDef(400202, 'Fraternal Castle Hidden Hourglass', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400203, 'Fraternal Castle Dragon Slayer', 'Fraternal Castle', LocationType.boss_reward, ItemType.unknown), + LocationDef(400204, 'Fraternal Castle Guru', 'Fraternal Castle', LocationType.give, ItemType.unknown), + + # Evil Fortress + LocationDef(400205, 'Evil Fortress Ointment', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400206, 'Evil Fortress Poison 1', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400207, 'Evil Fortress Glove', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400208, 'Evil Fortress Poison 2', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400209, 'Evil Fortress Poison 3', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400210, 'Evil Fortress Hidden Glove', 'Evil Fortress', LocationType.hidden, ItemType.unknown), + LocationDef(None, 'Evil One', 'Evil Fortress', LocationType.boss, ItemType.unknown), +] diff --git a/worlds/faxanadu/Options.py b/worlds/faxanadu/Options.py new file mode 100644 index 000000000000..dbcb5789944f --- /dev/null +++ b/worlds/faxanadu/Options.py @@ -0,0 +1,107 @@ +from Options import PerGameCommonOptions, Toggle, DefaultOnToggle, StartInventoryPool, Choice +from dataclasses import dataclass + + +class KeepShopRedPotions(Toggle): + """ + Prevents the Shop's Red Potions from being shuffled. Those locations + will have purchasable Red Potion as usual for their usual price. + """ + display_name = "Keep Shop Red Potions" + + +class IncludePendant(Toggle): + """ + Pendant is an item that boosts your attack power permanently when picked up. + However, due to a programming error in the original game, it has the reverse + effect. You start with the Pendant power, and lose it when picking + it up. So this item is essentially a trap. + There is a setting in the client to reverse the effect back to its original intend. + This could be used in conjunction with this option to increase or lower difficulty. + """ + display_name = "Include Pendant" + + +class IncludePoisons(DefaultOnToggle): + """ + Whether or not to include Poison Potions in the pool of items. Including them + effectively turn them into traps in multiplayer. + """ + display_name = "Include Poisons" + + +class RequireDragonSlayer(Toggle): + """ + Requires the Dragon Slayer to be available before fighting the final boss is required. + Turning this on will turn Progressive Shields into progression items. + + This setting does not force you to use Dragon Slayer to kill the final boss. + Instead, it ensures that you will have the Dragon Slayer and be able to equip + it before you are expected to beat the final boss. + """ + display_name = "Require Dragon Slayer" + + +class RandomMusic(Toggle): + """ + All levels' music is shuffled. Except the title screen because it's finite. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random Musics" + + +class RandomSound(Toggle): + """ + All sounds are shuffled. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random Sounds" + + +class RandomNPC(Toggle): + """ + NPCs and their portraits are shuffled. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random NPCs" + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + "Vanilla": No randomization + "Level Shuffle": Monsters are shuffled within a level + "Level Random": Monsters are picked randomly, balanced based on the ratio of the current level + "World Shuffle": Monsters are shuffled across the entire world + "World Random": Monsters are picked randomly, balanced based on the ratio of the entire world + "Chaotic": Completely random, except big vs small ratio is kept. Big are mini-bosses. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_level_shuffle = 1 + option_level_random = 2 + option_world_shuffle = 3 + option_world_random = 4 + option_chaotic = 5 + default = 0 + + +class RandomRewards(Toggle): + """ + Monsters drops are shuffled. + """ + display_name = "Random Rewards" + + +@dataclass +class FaxanaduOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + keep_shop_red_potions: KeepShopRedPotions + include_pendant: IncludePendant + include_poisons: IncludePoisons + require_dragon_slayer: RequireDragonSlayer + random_musics: RandomMusic + random_sounds: RandomSound + random_npcs: RandomNPC + random_monsters: RandomMonsters + random_rewards: RandomRewards diff --git a/worlds/faxanadu/Regions.py b/worlds/faxanadu/Regions.py new file mode 100644 index 000000000000..9db11d8ef114 --- /dev/null +++ b/worlds/faxanadu/Regions.py @@ -0,0 +1,66 @@ +from BaseClasses import Region +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import FaxanaduWorld + + +def create_region(name, player, multiworld): + region = Region(name, player, multiworld) + multiworld.regions.append(region) + return region + + +def create_regions(faxanadu_world: "FaxanaduWorld"): + player = faxanadu_world.player + multiworld = faxanadu_world.multiworld + + # Create regions + menu = create_region("Menu", player, multiworld) + eolis = create_region("Eolis", player, multiworld) + path_to_apolune = create_region("Path to Apolune", player, multiworld) + apolune = create_region("Apolune", player, multiworld) + create_region("Tower of Trunk", player, multiworld) + path_to_forepaw = create_region("Path to Forepaw", player, multiworld) + forepaw = create_region("Forepaw", player, multiworld) + trunk = create_region("Trunk", player, multiworld) + create_region("Joker Spring", player, multiworld) + create_region("Tower of Fortress", player, multiworld) + path_to_mascon = create_region("Path to Mascon", player, multiworld) + create_region("Tower of Red Potion", player, multiworld) + mascon = create_region("Mascon", player, multiworld) + path_to_victim = create_region("Path to Victim", player, multiworld) + create_region("Tower of Suffer", player, multiworld) + victim = create_region("Victim", player, multiworld) + mist = create_region("Mist", player, multiworld) + create_region("Useless Tower", player, multiworld) + create_region("Tower of Mist", player, multiworld) + path_to_conflate = create_region("Path to Conflate", player, multiworld) + create_region("Helm Branch", player, multiworld) + create_region("Conflate", player, multiworld) + branches = create_region("Branches", player, multiworld) + path_to_daybreak = create_region("Path to Daybreak", player, multiworld) + daybreak = create_region("Daybreak", player, multiworld) + dartmoor_castle = create_region("Dartmoor Castle", player, multiworld) + create_region("Dartmoor", player, multiworld) + create_region("Fraternal Castle", player, multiworld) + create_region("Evil Fortress", player, multiworld) + + # Create connections + menu.add_exits(["Eolis"]) + eolis.add_exits(["Path to Apolune"]) + path_to_apolune.add_exits(["Apolune"]) + apolune.add_exits(["Tower of Trunk", "Path to Forepaw"]) + path_to_forepaw.add_exits(["Forepaw"]) + forepaw.add_exits(["Trunk"]) + trunk.add_exits(["Joker Spring", "Tower of Fortress", "Path to Mascon"]) + path_to_mascon.add_exits(["Tower of Red Potion", "Mascon"]) + mascon.add_exits(["Path to Victim"]) + path_to_victim.add_exits(["Tower of Suffer", "Victim"]) + victim.add_exits(["Mist"]) + mist.add_exits(["Useless Tower", "Tower of Mist", "Path to Conflate"]) + path_to_conflate.add_exits(["Helm Branch", "Conflate", "Branches"]) + branches.add_exits(["Path to Daybreak"]) + path_to_daybreak.add_exits(["Daybreak"]) + daybreak.add_exits(["Dartmoor Castle"]) + dartmoor_castle.add_exits(["Dartmoor", "Fraternal Castle", "Evil Fortress"]) diff --git a/worlds/faxanadu/Rules.py b/worlds/faxanadu/Rules.py new file mode 100644 index 000000000000..a48b442c107a --- /dev/null +++ b/worlds/faxanadu/Rules.py @@ -0,0 +1,79 @@ +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import FaxanaduWorld + + +def can_buy_in_eolis(state, player): + # Sword or Deluge so we can farm for gold. + # Ring of Elf so we can get 1500 from the King. + return state.has_any(["Progressive Sword", "Deluge", "Ring of Elf"], player) + + +def has_any_magic(state, player): + return state.has_any(["Deluge", "Thunder", "Fire", "Death", "Tilte"], player) + + +def set_rules(faxanadu_world: "FaxanaduWorld"): + player = faxanadu_world.player + multiworld = faxanadu_world.multiworld + + # Region rules + set_rule(multiworld.get_entrance("Eolis -> Path to Apolune", player), lambda state: + state.has_all(["Key Jack", "Progressive Sword"], player)) # You can't go far with magic only + set_rule(multiworld.get_entrance("Apolune -> Tower of Trunk", player), lambda state: state.has("Key Jack", player)) + set_rule(multiworld.get_entrance("Apolune -> Path to Forepaw", player), lambda state: state.has("Mattock", player)) + set_rule(multiworld.get_entrance("Trunk -> Joker Spring", player), lambda state: state.has("Key Joker", player)) + set_rule(multiworld.get_entrance("Trunk -> Tower of Fortress", player), lambda state: state.has("Key Jack", player)) + set_rule(multiworld.get_entrance("Trunk -> Path to Mascon", player), lambda state: + state.has_all(["Key Queen", "Ring of Ruby", "Sky Spring Flow", "Tower of Fortress Spring Flow", "Joker Spring Flow"], player) and + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_entrance("Path to Mascon -> Tower of Red Potion", player), lambda state: + state.has("Key Queen", player) and + state.has("Red Potion", player, 4)) # It's impossible to go through the tower of Red Potion without at least 1-2 potions. Give them 4 for good measure. + set_rule(multiworld.get_entrance("Path to Victim -> Tower of Suffer", player), lambda state: state.has("Key Queen", player)) + set_rule(multiworld.get_entrance("Path to Victim -> Victim", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_entrance("Mist -> Useless Tower", player), lambda state: + state.has_all(["Key King", "Unlock Wingboots"], player)) + set_rule(multiworld.get_entrance("Mist -> Tower of Mist", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Mist -> Path to Conflate", player), lambda state: state.has("Key Ace", player)) + set_rule(multiworld.get_entrance("Path to Conflate -> Helm Branch", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Path to Conflate -> Branches", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Daybreak -> Dartmoor Castle", player), lambda state: state.has("Ring of Dworf", player)) + set_rule(multiworld.get_entrance("Dartmoor Castle -> Evil Fortress", player), lambda state: state.has("Demons Ring", player)) + + # Location rules + set_rule(multiworld.get_location("Eolis Key Jack", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Hand Dagger", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Elixir", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Deluge", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Red Potion", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Path to Apolune Magic Shield", player), lambda state: state.has("Key King", player)) # Mid-late cost, make sure we've progressed + set_rule(multiworld.get_location("Path to Apolune Death", player), lambda state: state.has("Key Ace", player)) # Mid-late cost, make sure we've progressed + set_rule(multiworld.get_location("Tower of Trunk Hidden Mattock", player), lambda state: + # This is actually possible if the monster drop into the stairs and kill it with dagger. But it's a "pro move" + state.has("Deluge", player, 1) or + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_location("Path to Forepaw Glove", player), lambda state: + state.has_all(["Deluge", "Unlock Wingboots"], player)) + set_rule(multiworld.get_location("Trunk Red Potion", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Sky Spring", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Tower of Fortress Spring", player), lambda state: state.has("Spring Elixir", player)) + set_rule(multiworld.get_location("Tower of Fortress Guru", player), lambda state: state.has("Sky Spring Flow", player)) + set_rule(multiworld.get_location("Tower of Suffer Hidden Wingboots", player), lambda state: + state.has("Deluge", player) or + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_location("Misty House", player), lambda state: state.has("Black Onyx", player)) + set_rule(multiworld.get_location("Misty Doctor Office", player), lambda state: has_any_magic(state, player)) + set_rule(multiworld.get_location("Conflate Guru", player), lambda state: state.has("Progressive Armor", player, 3)) + set_rule(multiworld.get_location("Branches Hidden Mattock", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Path to Daybreak Glove", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Dartmoor Castle Hidden Hourglass", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Dartmoor Castle Hidden Red Potion", player), lambda state: has_any_magic(state, player)) + set_rule(multiworld.get_location("Fraternal Castle Guru", player), lambda state: state.has("Progressive Sword", player, 4)) + set_rule(multiworld.get_location("Fraternal Castle Shop Hidden Ointment", player), lambda state: has_any_magic(state, player)) + + if faxanadu_world.options.require_dragon_slayer.value: + set_rule(multiworld.get_location("Evil One", player), lambda state: + state.has_all_counts({"Progressive Sword": 4, "Progressive Armor": 3, "Progressive Shield": 4}, player)) diff --git a/worlds/faxanadu/__init__.py b/worlds/faxanadu/__init__.py new file mode 100644 index 000000000000..c4ae1ccaa198 --- /dev/null +++ b/worlds/faxanadu/__init__.py @@ -0,0 +1,190 @@ +from typing import Any, Dict, List + +from BaseClasses import Item, Location, Tutorial, ItemClassification, MultiWorld +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Regions, Rules +from .Options import FaxanaduOptions +from worlds.generic.Rules import set_rule + + +DAXANADU_VERSION = "0.3.0" + + +class FaxanaduLocation(Location): + game: str = "Faxanadu" + + +class FaxanaduItem(Item): + game: str = "Faxanadu" + + +class FaxanaduWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Faxanadu randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class FaxanaduWorld(World): + """ + Faxanadu is an action role-playing platform video game developed by Hudson Soft for the Nintendo Entertainment System + """ + options_dataclass = FaxanaduOptions + options: FaxanaduOptions + game = "Faxanadu" + web = FaxanaduWeb() + + item_name_to_id = {item.name: item.id for item in Items.items if item.id is not None} + item_name_to_item = {item.name: item for item in Items.items} + location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None} + + def __init__(self, world: MultiWorld, player: int): + self.filler_ratios: Dict[str, int] = {} + + super().__init__(world, player) + + def create_regions(self): + Regions.create_regions(self) + + # Add locations into regions + for region in self.multiworld.get_regions(self.player): + for loc in [location for location in Locations.locations if location.region == region.name]: + location = FaxanaduLocation(self.player, loc.name, loc.id, region) + + # In Faxanadu, Poison hurts you when picked up. It makes no sense to sell them in shops + if loc.type == Locations.LocationType.shop: + location.item_rule = lambda item, player=self.player: not (player == item.player and item.name == "Poison") + + region.locations.append(location) + + def set_rules(self): + Rules.set_rules(self) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Killed Evil One", self.player) + + def create_item(self, name: str) -> FaxanaduItem: + item: Items.ItemDef = self.item_name_to_item[name] + return FaxanaduItem(name, item.classification, item.id, self.player) + + # Returns how many red potions were prefilled into shops + def prefill_shop_red_potions(self) -> int: + red_potion_in_shop_count = 0 + if self.options.keep_shop_red_potions: + red_potion_item = self.item_name_to_item["Red Potion"] + red_potion_shop_locations = [ + loc + for loc in Locations.locations + if loc.type == Locations.LocationType.shop and loc.original_item == Locations.ItemType.red_potion + ] + for loc in red_potion_shop_locations: + location = self.get_location(loc.name) + location.place_locked_item(FaxanaduItem(red_potion_item.name, red_potion_item.classification, red_potion_item.id, self.player)) + red_potion_in_shop_count += 1 + return red_potion_in_shop_count + + def put_wingboot_in_shop(self, shops, region_name): + item = self.item_name_to_item["Wingboots"] + shop = shops.pop(region_name) + slot = self.random.randint(0, len(shop) - 1) + loc = shop[slot] + location = self.get_location(loc.name) + location.place_locked_item(FaxanaduItem(item.name, item.classification, item.id, self.player)) + + # Put a rule right away that we need to have to unlocked. + set_rule(location, lambda state: state.has("Unlock Wingboots", self.player)) + + # Returns how many wingboots were prefilled into shops + def prefill_shop_wingboots(self) -> int: + # Collect shops + shops: Dict[str, List[Locations.LocationDef]] = {} + for loc in Locations.locations: + if loc.type == Locations.LocationType.shop: + if self.options.keep_shop_red_potions and loc.original_item == Locations.ItemType.red_potion: + continue # Don't override our red potions + shops.setdefault(loc.region, []).append(loc) + + shop_count = len(shops) + wingboots_count = round(shop_count / 2.5) # On 10 shops, we should have about 4 shops with wingboots + + # At least one should be in the first 4 shops. Because we require wingboots to progress past that point. + must_have_regions = [region for i, region in enumerate(shops) if i < 4] + self.put_wingboot_in_shop(shops, self.random.choice(must_have_regions)) + + # Fill in the rest randomly in remaining shops + for i in range(wingboots_count - 1): # -1 because we added one already + region = self.random.choice(list(shops.keys())) + self.put_wingboot_in_shop(shops, region) + + return wingboots_count + + def create_items(self) -> None: + itempool: List[FaxanaduItem] = [] + + # Prefill red potions in shops if option is set + red_potion_in_shop_count = self.prefill_shop_red_potions() + + # Prefill wingboots in shops + wingboots_in_shop_count = self.prefill_shop_wingboots() + + # Create the item pool, excluding fillers. + prefilled_count = red_potion_in_shop_count + wingboots_in_shop_count + for item in Items.items: + # Ignore pendant if turned off + if item.name == "Pendant" and not self.options.include_pendant: + continue + + # ignore fillers for now, we will fill them later + if item.classification in [ItemClassification.filler, ItemClassification.trap] and \ + item.progression_count == 0: + continue + + prefill_loc = None + if item.prefill_location: + prefill_loc = self.get_location(item.prefill_location) + + # if require dragon slayer is turned on, we need progressive shields to be progression + item_classification = item.classification + if self.options.require_dragon_slayer and item.name == "Progressive Shield": + item_classification = ItemClassification.progression + + if prefill_loc: + prefill_loc.place_locked_item(FaxanaduItem(item.name, item_classification, item.id, self.player)) + prefilled_count += 1 + else: + for i in range(item.count - item.progression_count): + itempool.append(FaxanaduItem(item.name, item_classification, item.id, self.player)) + for i in range(item.progression_count): + itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player)) + + # Set up filler ratios + self.filler_ratios = { + item.name: item.count + for item in Items.items + if item.classification in [ItemClassification.filler, ItemClassification.trap] + } + + # If red potions are locked in shops, remove the count from the ratio. + self.filler_ratios["Red Potion"] -= red_potion_in_shop_count + + # Remove poisons if not desired + if not self.options.include_poisons: + self.filler_ratios["Poison"] = 0 + + # Randomly add fillers to the pool with ratios based on og game occurrence counts. + filler_count = len(Locations.locations) - len(itempool) - prefilled_count + for i in range(filler_count): + itempool.append(self.create_item(self.get_filler_item_name())) + + self.multiworld.itempool += itempool + + def get_filler_item_name(self) -> str: + return self.random.choices(list(self.filler_ratios.keys()), weights=list(self.filler_ratios.values()))[0] + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict("keep_shop_red_potions", "random_musics", "random_sounds", "random_npcs", "random_monsters", "random_rewards") + slot_data["daxanadu_version"] = DAXANADU_VERSION + return slot_data diff --git a/worlds/faxanadu/docs/en_Faxanadu.md b/worlds/faxanadu/docs/en_Faxanadu.md new file mode 100644 index 000000000000..7f5c4ab293ce --- /dev/null +++ b/worlds/faxanadu/docs/en_Faxanadu.md @@ -0,0 +1,27 @@ +# Faxanadu + +## Where is the settings page? + +The [player options page](../player-options) contains the options needed to configure your game session. + +## What does randomization do to this game? + +All game items collected in the map, shops, and boss drops are randomized. + +Keys are unique. Once you get the Jack Key, you can open all Jack doors; the key stays in your inventory. + +Wingboots are randomized across shops only. They are LOCKED and cannot be bought until you get the item that unlocks them. + +Normal Elixirs don't revive the tower spring. A new item, Spring Elixir, is necessary. This new item is unique. + +## What is the goal? + +The goal is to kill the Evil One. + +## What is a "check" in The Faxanadu? + +Shop items, item locations in the world, boss drops, and secret items. + +## What "items" can you unlock in Faxanadu? + +Keys, Armors, Weapons, Potions, Shields, Magics, Poisons, Gloves, etc. diff --git a/worlds/faxanadu/docs/setup_en.md b/worlds/faxanadu/docs/setup_en.md new file mode 100644 index 000000000000..4ff714c61393 --- /dev/null +++ b/worlds/faxanadu/docs/setup_en.md @@ -0,0 +1,32 @@ +# Faxanadu Randomizer Setup + +## Required Software + +- [Daxanadu](https://github.com/Daivuk/Daxanadu/releases/) +- Faxanadu ROM, English version + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing Daxanadu +1. Download [Daxanadu.zip](https://github.com/Daivuk/Daxanadu/releases/) and extract it. +2. Copy your rom `Faxanadu (U).nes` into the newly extracted folder. + +## Joining a MultiWorld Game + +1. Launch Daxanadu.exe +2. From the Main menu, go to the `ARCHIPELAGO` menu. Enter the server's address, slot name, and password. Then select `PLAY`. +3. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +Daxanadu doesn't display messages. You'll only get popups when picking them up. + +## Auto-Tracking + +Daxanadu has an integrated tracker that can be toggled in the options. From 1365bd7a0ac0a130de3f383505140091ef8a0683 Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Fri, 29 Nov 2024 13:46:38 -0800 Subject: [PATCH 040/112] CODEOWNERS: Add KScl as world maintainer for id Tech 1 games (#4288) --- docs/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 4b7496c686c8..64a1362bf380 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -55,10 +55,10 @@ /worlds/dlcquest/ @axe-y @agilbert1412 # DOOM 1993 -/worlds/doom_1993/ @Daivuk +/worlds/doom_1993/ @Daivuk @KScl # DOOM II -/worlds/doom_ii/ @Daivuk +/worlds/doom_ii/ @Daivuk @KScl # Factorio /worlds/factorio/ @Berserker66 @@ -70,7 +70,7 @@ /worlds/ffmq/ @Alchav @wildham0 # Heretic -/worlds/heretic/ @Daivuk +/worlds/heretic/ @Daivuk @KScl # Hollow Knight /worlds/hk/ @BadMagic100 @qwint From 7c00c9a49d4f1626c049ad132e64da2d09f5e818 Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:48:01 +0100 Subject: [PATCH 041/112] Core: Change "Unreachable Items" to "Unreachable progression items" in playthrough warning for clarification (#4287) --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 700a21506ac8..d84da5d212b4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1545,7 +1545,7 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: [f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else [f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) if self.unreachables: - outfile.write('\n\nUnreachable Items:\n\n') + outfile.write('\n\nUnreachable Progression Items:\n\n') outfile.write( '\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) From b0a61be9df713e31ec874d84326d26f3d3e225ac Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:57:35 -0500 Subject: [PATCH 042/112] Tests: Add test that local/non local items aren't modified late #3976 --- test/general/test_items.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/general/test_items.py b/test/general/test_items.py index 9cc91a1b00ef..64ce1b6997b7 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -80,3 +80,21 @@ def test_itempool_not_modified(self): call_all(multiworld, step) self.assertEqual(created_items, multiworld.itempool, f"{game_name} modified the itempool during {step}") + + def test_locality_not_modified(self): + """Test that worlds don't modify the locality of items after duplicates are resolved""" + gen_steps = ("generate_early", "create_regions", "create_items") + additional_steps = ("set_rules", "generate_basic", "pre_fill") + worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()} + for game_name, world_type in worlds_to_test.items(): + with self.subTest("Game", game=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + local_items = multiworld.worlds[1].options.local_items.value.copy() + non_local_items = multiworld.worlds[1].options.non_local_items.value.copy() + for step in additional_steps: + with self.subTest("step", step=step): + call_all(multiworld, step) + self.assertEqual(local_items, multiworld.worlds[1].options.local_items.value, + f"{game_name} modified local_items during {step}") + self.assertEqual(non_local_items, multiworld.worlds[1].options.non_local_items.value, + f"{game_name} modified non_local_items during {step}") From 46dfc4d4fc00d61e89a2b3885053d003df7bae81 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 29 Nov 2024 16:37:14 -0600 Subject: [PATCH 043/112] Core: Allow option groups to specify option order (#3393) * Core: Allow option groups to specify option order * words hard * Actually use the earlier built dictionary for faster in checking Co-authored-by: Doug Hoskisson --------- Co-authored-by: Doug Hoskisson --- Options.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/Options.py b/Options.py index d81f81face06..0d0e6ae1cd95 100644 --- a/Options.py +++ b/Options.py @@ -1463,22 +1463,26 @@ class OptionGroup(typing.NamedTuple): def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: """Generates and returns a dictionary for the option groups of a specified world.""" - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} + option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()} + + ordered_groups = {group.name: group.options for group in world.web.option_groups} + # add a default option group for uncategorized options to get thrown into - 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 visibility_level & option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option - - # if the world doesn't have any ungrouped options, this group will be empty so just remove it - if not grouped_options["Game Options"]: - del grouped_options["Game Options"] - - return grouped_options + if "Game Options" not in ordered_groups: + grouped_options = set(option for group in ordered_groups.values() for option in group) + ungrouped_options = [option for option in option_to_name if option not in grouped_options] + # only add the game options group if we have ungrouped options + if ungrouped_options: + ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups} + + return { + group: { + option_to_name[option]: option + for option in group_options + if (visibility_level in option.visibility and option in option_to_name) + } + for group, group_options in ordered_groups.items() + } def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: From dd88b2c6581e9f5db97dc3601086637f34c52b5f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:47:27 +0100 Subject: [PATCH 044/112] The Witness: Fix unreachable locations on Longbox + Postgame #4291 --- worlds/witness/player_logic.py | 2 +- worlds/witness/test/test_roll_other_options.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index ec4ea066e579..58f15532f58c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -984,7 +984,7 @@ def make_event_panel_lists(self) -> None: Makes event-item pairs for entities with associated events, unless these entities are disabled. """ - self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" + self.USED_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION].append("Victory") for event_hex, event_name in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): self.USED_EVENT_NAMES_BY_HEX[event_hex].append(event_name) diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py index 0429b097eac3..05f3235a1f4d 100644 --- a/worlds/witness/test/test_roll_other_options.py +++ b/worlds/witness/test/test_roll_other_options.py @@ -62,3 +62,10 @@ class TestPostgameGroupedDoors(WitnessTestBase): "door_groupings": "regional", "victory_condition": "elevator", } + + +class TestPostgamePanels(WitnessTestBase): + options = { + "victory_condition": "mountain_box_long", + "shuffle_postgame": True + } From 1487d323cd165270c85ebf938b47cf1726a33900 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 29 Nov 2024 18:01:24 -0500 Subject: [PATCH 045/112] Core: update error message for mismatched "event" placements #4043 --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 606514630f0d..b9612e9549ba 100644 --- a/Main.py +++ b/Main.py @@ -282,7 +282,7 @@ def precollect_hint(location): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ "location.address should then also be None. Location: " \ - f" {location}" + f" {location}, Item: {location.item}" assert location.address not in locations_data[location.player], ( f"Locations with duplicate address. {location} and " f"{locations_data[location.player][location.address]}") From 492e3a355e5f9ec341e3b27fc522372374de1cea Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 30 Nov 2024 00:37:26 +0100 Subject: [PATCH 046/112] WebHost: delete unused script tag (#4062) --- WebHostLib/templates/templates.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/WebHostLib/templates/templates.html b/WebHostLib/templates/templates.html index fb6ea7e9eab5..3b2418ae15b6 100644 --- a/WebHostLib/templates/templates.html +++ b/WebHostLib/templates/templates.html @@ -4,9 +4,6 @@ {% include 'header/grassHeader.html' %} Option Templates (YAML) - {% endblock %} {% block body %} From e262c8be9cc6b4c315673e9c7675fdf0d1328c1d Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:46:35 -0500 Subject: [PATCH 047/112] Stardew Valley: Fix a bug where locations in logic would disappear from universal tracker as items get sent (#4230) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/stardew_valley/__init__.py | 72 ++++++++++++------- worlds/stardew_valley/items.py | 13 ++-- worlds/stardew_valley/stardew_rule/state.py | 12 +++- worlds/stardew_valley/test/TestCrops.py | 8 +-- worlds/stardew_valley/test/TestOptions.py | 19 ++++- worlds/stardew_valley/test/__init__.py | 33 ++++----- .../stardew_valley/test/rules/TestArcades.py | 52 +++++++------- .../test/rules/TestBuildings.py | 8 +-- .../test/rules/TestCookingRecipes.py | 32 ++++----- .../test/rules/TestCraftingRecipes.py | 30 ++++---- .../test/rules/TestDonations.py | 6 +- .../test/rules/TestFriendship.py | 34 ++++----- .../stardew_valley/test/rules/TestShipping.py | 2 +- .../test/rules/TestStateRules.py | 28 +++++--- worlds/stardew_valley/test/rules/TestTools.py | 40 +++++------ .../stardew_valley/test/rules/TestWeapons.py | 48 ++++++------- .../test/stability/TestStability.py | 7 +- 17 files changed, 242 insertions(+), 202 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 01ca56531f00..135afa1c9726 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -3,7 +3,7 @@ from typing import Dict, Any, Iterable, Optional, Union, List, TextIO from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState -from Options import PerGameCommonOptions +from Options import PerGameCommonOptions, Accessibility from worlds.AutoWorld import World, WebWorld from . import rules from .bundles.bundle_room import BundleRoom @@ -91,15 +91,14 @@ class StardewValleyWorld(World): web = StardewWebWorld() modified_bundles: List[BundleRoom] randomized_entrances: Dict[str, str] - total_progression_items: int - # all_progression_items: Dict[str, int] # If you need to debug total_progression_items, uncommenting this will help tremendously + total_progression_items: int + excluded_from_total_progression_items: List[str] = [Event.received_walnuts] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) self.filler_item_pool_names = [] self.total_progression_items = 0 - # self.all_progression_items = dict() # Taking the seed specified in slot data for UT, otherwise just generating the seed. self.seed = getattr(multiworld, "re_gen_passthrough", {}).get(STARDEW_VALLEY, self.random.getrandbits(64)) @@ -121,17 +120,27 @@ def force_change_options_if_incompatible(self): goal_is_perfection = self.options.goal == Goal.option_perfection goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection exclude_ginger_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true + if goal_is_island_related and exclude_ginger_island: self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false goal_name = self.options.goal.current_key - player_name = self.multiworld.player_name[self.player] logger.warning( - f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") + f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({self.player_name})") + if exclude_ginger_island and self.options.walnutsanity != Walnutsanity.preset_none: self.options.walnutsanity.value = Walnutsanity.preset_none - player_name = self.multiworld.player_name[self.player] logger.warning( - f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({player_name})'s world, so walnutsanity was force disabled") + f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({self.player_name})'s world, so walnutsanity was force disabled") + + if goal_is_perfection and self.options.accessibility == Accessibility.option_minimal: + self.options.accessibility.value = Accessibility.option_full + logger.warning( + f"Goal 'Perfection' requires full accessibility. Accessibility setting forced to 'Full' for player {self.player} ({self.player_name})") + + elif self.options.goal == Goal.option_allsanity and self.options.accessibility == Accessibility.option_minimal: + self.options.accessibility.value = Accessibility.option_full + logger.warning( + f"Goal 'Allsanity' requires full accessibility. Accessibility setting forced to 'Full' for player {self.player} ({self.player_name})") def create_regions(self): def create_region(name: str, exits: Iterable[str]) -> Region: @@ -171,8 +180,7 @@ def create_items(self): for location in self.multiworld.get_locations(self.player) if location.address is not None]) - created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, self.content, - self.random) + created_items = create_items(self.create_item, locations_count, items_to_exclude, self.options, self.content, self.random) self.multiworld.itempool += created_items @@ -180,6 +188,18 @@ def create_items(self): self.setup_player_events() self.setup_victory() + # This is really a best-effort to get the total progression items count. It is mostly used to spread grinds across spheres are push back locations that + # only become available after months or years in game. In most cases, not having the exact count will not impact the logic. + # + # The actual total can be impacted by the start_inventory_from_pool, when items are removed from the pool but not from the total. The is also a bug + # with plando where additional progression items can be created without being accounted for, which impact the real amount of progression items. This can + # ultimately create unwinnable seeds where some items (like Blueberry seeds) are locked in Shipsanity: Blueberry, but world is deemed winnable as the + # winning rule only check the count of collected progression items. + self.total_progression_items += sum(1 for i in self.multiworld.precollected_items[self.player] if i.advancement) + self.total_progression_items += sum(1 for i in self.multiworld.get_filled_locations(self.player) if i.advancement) + self.total_progression_items += sum(1 for i in created_items if i.advancement) + self.total_progression_items -= 1 # -1 for the victory event + def precollect_starting_season(self): if self.options.season_randomization == SeasonRandomization.option_progressive: return @@ -304,14 +324,8 @@ def create_item(self, item: Union[str, ItemData], override_classification: ItemC if override_classification is None: override_classification = item.classification - if override_classification & ItemClassification.progression: - self.total_progression_items += 1 return StardewItem(item.name, override_classification, item.code, self.player) - def delete_item(self, item: Item): - if item.classification & ItemClassification.progression: - self.total_progression_items -= 1 - def create_starting_item(self, item: Union[str, ItemData]) -> StardewItem: if isinstance(item, str): item = item_table[item] @@ -330,10 +344,6 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule = region.locations.append(location) location.place_locked_item(StardewItem(item, ItemClassification.progression, None, self.player)) - # This is not ideal, but the rule count them so... - if item != Event.victory: - self.total_progression_items += 1 - def set_rules(self): set_rules(self) @@ -426,15 +436,25 @@ def fill_slot_data(self) -> Dict[str, Any]: def collect(self, state: CollectionState, item: StardewItem) -> bool: change = super().collect(state, item) - if change: - state.prog_items[self.player][Event.received_walnuts] += self.get_walnut_amount(item.name) - return change + if not change: + return False + + walnut_amount = self.get_walnut_amount(item.name) + if walnut_amount: + state.prog_items[self.player][Event.received_walnuts] += walnut_amount + + return True def remove(self, state: CollectionState, item: StardewItem) -> bool: change = super().remove(state, item) - if change: - state.prog_items[self.player][Event.received_walnuts] -= self.get_walnut_amount(item.name) - return change + if not change: + return False + + walnut_amount = self.get_walnut_amount(item.name) + if walnut_amount: + state.prog_items[self.player][Event.received_walnuts] -= walnut_amount + + return True @staticmethod def get_walnut_amount(item_name: str) -> int: diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 993863bf5bf5..5bf59a5e8397 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -169,14 +169,14 @@ def get_too_many_items_error_message(locations_count: int, items_count: int) -> return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]" -def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item], +def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item], options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] unique_items = create_unique_items(item_factory, options, content, random) - remove_items(item_deleter, items_to_exclude, unique_items) + remove_items(items_to_exclude, unique_items) - remove_items_if_no_room_for_them(item_deleter, unique_items, locations_count, random) + remove_items_if_no_room_for_them(unique_items, locations_count, random) items += unique_items logger.debug(f"Created {len(unique_items)} unique items") @@ -192,14 +192,13 @@ def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDele return items -def remove_items(item_deleter: StardewItemDeleter, items_to_remove, items): +def remove_items(items_to_remove, items): for item in items_to_remove: if item in items: items.remove(item) - item_deleter(item) -def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_items: List[Item], locations_count: int, random: Random): +def remove_items_if_no_room_for_them(unique_items: List[Item], locations_count: int, random: Random): if len(unique_items) <= locations_count: return @@ -212,7 +211,7 @@ def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_it logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random filler items") assert len(removable_items) >= number_of_items_to_remove, get_too_many_items_error_message(locations_count, len(unique_items)) items_to_remove = random.sample(removable_items, number_of_items_to_remove) - remove_items(item_deleter, items_to_remove, unique_items) + remove_items(items_to_remove, unique_items) def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index 5f5e61b3d4e5..6fc349a6274d 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -1,10 +1,13 @@ from dataclasses import dataclass -from typing import Iterable, Union, List, Tuple, Hashable +from typing import Iterable, Union, List, Tuple, Hashable, TYPE_CHECKING from BaseClasses import CollectionState from .base import BaseStardewRule, CombinableStardewRule from .protocol import StardewRule +if TYPE_CHECKING: + from .. import StardewValleyWorld + class TotalReceived(BaseStardewRule): count: int @@ -102,16 +105,19 @@ def value(self): return self.percent def __call__(self, state: CollectionState) -> bool: - stardew_world = state.multiworld.worlds[self.player] + stardew_world: "StardewValleyWorld" = state.multiworld.worlds[self.player] total_count = stardew_world.total_progression_items needed_count = (total_count * self.percent) // 100 player_state = state.prog_items[self.player] - if needed_count <= len(player_state): + if needed_count <= len(player_state) - len(stardew_world.excluded_from_total_progression_items): return True total_count = 0 for item, item_count in player_state.items(): + if item in stardew_world.excluded_from_total_progression_items: + continue + total_count += item_count if total_count >= needed_count: return True diff --git a/worlds/stardew_valley/test/TestCrops.py b/worlds/stardew_valley/test/TestCrops.py index 362e6bf27e7c..4fa836a97d14 100644 --- a/worlds/stardew_valley/test/TestCrops.py +++ b/worlds/stardew_valley/test/TestCrops.py @@ -11,10 +11,10 @@ def test_need_greenhouse_for_cactus(self): harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit") self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), prevent_sweep=False) - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Cactus Seeds")) + self.multiworld.state.collect(self.create_item("Shipping Bin")) + self.multiworld.state.collect(self.create_item("Desert Obelisk")) self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Greenhouse"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Greenhouse")) self.assert_rule_true(harvest_cactus, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 2824a10c38af..9db7f06ff5a5 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,6 +1,6 @@ import itertools -from Options import NamedRange +from Options import NamedRange, Accessibility from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld from .assertion import WorldAssertMixin from .long.option_names import all_option_choices @@ -54,6 +54,23 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): victory = multi_world.find_item("Victory", 1) self.assertEqual(victory.name, location) + def test_given_perfection_goal_when_generate_then_accessibility_is_forced_to_full(self): + """There is a bug with the current victory condition of the perfection goal that can create unwinnable seeds if the accessibility is set to minimal and + the world gets flooded with progression items through plando. This will increase the amount of collected progression items pass the total amount + calculated for the world when creating the item pool. This will cause the victory condition to be met before all locations are collected, so some could + be left inaccessible, which in practice will make the seed unwinnable. + """ + for accessibility in Accessibility.options.keys(): + world_options = {Goal.internal_name: Goal.option_perfection, "accessibility": accessibility} + with self.solo_world_sub_test(f"Accessibility: {accessibility}", world_options) as (_, world): + self.assertEqual(world.options.accessibility, Accessibility.option_full) + + def test_given_allsanity_goal_when_generate_then_accessibility_is_forced_to_full(self): + for accessibility in Accessibility.options.keys(): + world_options = {Goal.internal_name: Goal.option_allsanity, "accessibility": accessibility} + with self.solo_world_sub_test(f"Accessibility: {accessibility}", world_options) as (_, world): + self.assertEqual(world.options.accessibility, Accessibility.option_full) + class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 8f4e5af28f84..1a312e569d11 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item, ItemClassification +from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item from Options import VerifyKeys from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld @@ -236,7 +236,6 @@ def world_setup(self, *args, **kwargs): self.original_state = self.multiworld.state.copy() self.original_itempool = self.multiworld.itempool.copy() - self.original_prog_item_count = world.total_progression_items self.unfilled_locations = self.multiworld.get_unfilled_locations(1) if self.constructed: self.world = world # noqa @@ -246,7 +245,6 @@ def tearDown(self) -> None: self.multiworld.itempool = self.original_itempool for location in self.unfilled_locations: location.item = None - self.world.total_progression_items = self.original_prog_item_count self.multiworld.lock.release() @@ -257,20 +255,13 @@ def run_default_tests(self) -> bool: return super().run_default_tests def collect_lots_of_money(self, percent: float = 0.25): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items + self.collect("Shipping Bin") + real_total_prog_items = self.world.total_progression_items required_prog_items = int(round(real_total_prog_items * percent)) - for i in range(required_prog_items): - self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) - self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items + self.collect("Stardrop", required_prog_items) def collect_all_the_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items - required_prog_items = int(round(real_total_prog_items * 0.95)) - for i in range(required_prog_items): - self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) - self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items + self.collect_lots_of_money(0.95) def collect_everything(self): non_event_items = [item for item in self.multiworld.get_items() if item.code] @@ -278,7 +269,8 @@ def collect_everything(self): self.multiworld.state.collect(item) def collect_all_except(self, item_to_not_collect: str): - for item in self.multiworld.get_items(): + non_event_items = [item for item in self.multiworld.get_items() if item.code] + for item in non_event_items: if item.name != item_to_not_collect: self.multiworld.state.collect(item) @@ -290,25 +282,26 @@ def get_real_location_names(self) -> List[str]: def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Union[None, Item, List[Item]]: assert count > 0 + if not isinstance(item, str): super().collect(item) return + if count == 1: item = self.create_item(item) self.multiworld.state.collect(item) return item + items = [] for i in range(count): item = self.create_item(item) self.multiworld.state.collect(item) items.append(item) + return items def create_item(self, item: str) -> StardewItem: - created_item = self.world.create_item(item) - if created_item.classification & ItemClassification.progression: - self.multiworld.worlds[self.player].total_progression_items -= 1 - return created_item + return self.world.create_item(item) def remove_one_by_name(self, item: str) -> None: self.remove(self.create_item(item)) @@ -336,7 +329,6 @@ def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption] original_state = multiworld.state.copy() original_itempool = multiworld.itempool.copy() unfilled_locations = multiworld.get_unfilled_locations(1) - original_prog_item_count = world.total_progression_items yield multiworld, world @@ -344,7 +336,6 @@ def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption] multiworld.itempool = original_itempool for location in unfilled_locations: location.item = None - multiworld.total_progression_items = original_prog_item_count multiworld.lock.release() diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py index 2922ecfb5d9e..69e5b22cc01b 100644 --- a/worlds/stardew_valley/test/rules/TestArcades.py +++ b/worlds/stardew_valley/test/rules/TestArcades.py @@ -19,8 +19,8 @@ def test_prairie_king(self): life = self.create_item("JotPK: Extra Life") drop = self.create_item("JotPK: Increased Drop Rate") - self.multiworld.state.collect(boots, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) @@ -28,8 +28,8 @@ def test_prairie_king(self): self.remove(boots) self.remove(gun) - self.multiworld.state.collect(boots, prevent_sweep=True) - self.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(boots) self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) @@ -37,10 +37,10 @@ def test_prairie_king(self): self.remove(boots) self.remove(boots) - self.multiworld.state.collect(boots, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(ammo, prevent_sweep=True) - self.multiworld.state.collect(life, prevent_sweep=True) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(life) self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) @@ -50,13 +50,13 @@ def test_prairie_king(self): self.remove(ammo) self.remove(life) - self.multiworld.state.collect(boots, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(ammo, prevent_sweep=True) - self.multiworld.state.collect(ammo, prevent_sweep=True) - self.multiworld.state.collect(life, prevent_sweep=True) - self.multiworld.state.collect(drop, prevent_sweep=True) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(life) + self.multiworld.state.collect(drop) self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) @@ -69,17 +69,17 @@ def test_prairie_king(self): self.remove(life) self.remove(drop) - self.multiworld.state.collect(boots, prevent_sweep=True) - self.multiworld.state.collect(boots, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(gun, prevent_sweep=True) - self.multiworld.state.collect(ammo, prevent_sweep=True) - self.multiworld.state.collect(ammo, prevent_sweep=True) - self.multiworld.state.collect(ammo, prevent_sweep=True) - self.multiworld.state.collect(life, prevent_sweep=True) - self.multiworld.state.collect(drop, prevent_sweep=True) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(life) + self.multiworld.state.collect(drop) self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py index cacd6ea381b6..d1f60b20e0db 100644 --- a/worlds/stardew_valley/test/rules/TestBuildings.py +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -23,7 +23,7 @@ def test_big_coop_blueprint(self): self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Coop")) self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") @@ -33,10 +33,10 @@ def test_deluxe_coop_blueprint(self): self.collect_lots_of_money() self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Coop")) self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Coop")) self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) def test_big_shed_blueprint(self): @@ -48,6 +48,6 @@ def test_big_shed_blueprint(self): self.assertFalse(big_shed_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - self.multiworld.state.collect(self.create_item("Progressive Shed"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Shed")) self.assertTrue(big_shed_rule(self.multiworld.state), f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") diff --git a/worlds/stardew_valley/test/rules/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py index 7ab9d61cb942..d5f9da73c9d7 100644 --- a/worlds/stardew_valley/test/rules/TestCookingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py @@ -17,14 +17,14 @@ def test_can_learn_qos_recipe(self): rule = self.world.logic.region.can_reach_location(location) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Radish Seeds"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Spring"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive House")) + self.multiworld.state.collect(self.create_item("Radish Seeds")) + self.multiworld.state.collect(self.create_item("Spring")) + self.multiworld.state.collect(self.create_item("Summer")) self.collect_lots_of_money() self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("The Queen of Sauce"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("The Queen of Sauce")) self.assert_rule_true(rule, self.multiworld.state) @@ -42,21 +42,21 @@ def test_can_learn_qos_recipe(self): rule = self.world.logic.region.can_reach_location(location) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Radish Seeds"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive House")) + self.multiworld.state.collect(self.create_item("Radish Seeds")) + self.multiworld.state.collect(self.create_item("Summer")) self.collect_lots_of_money() self.assert_rule_false(rule, self.multiworld.state) spring = self.create_item("Spring") qos = self.create_item("The Queen of Sauce") - self.multiworld.state.collect(spring, prevent_sweep=False) - self.multiworld.state.collect(qos, prevent_sweep=False) + self.multiworld.state.collect(spring) + self.multiworld.state.collect(qos) self.assert_rule_false(rule, self.multiworld.state) self.multiworld.state.remove(spring) self.multiworld.state.remove(qos) - self.multiworld.state.collect(self.create_item("Radish Salad Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Radish Salad Recipe")) self.assert_rule_true(rule, self.multiworld.state) def test_get_chefsanity_check_recipe(self): @@ -64,20 +64,20 @@ def test_get_chefsanity_check_recipe(self): rule = self.world.logic.region.can_reach_location(location) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Spring"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Spring")) self.collect_lots_of_money() self.assert_rule_false(rule, self.multiworld.state) seeds = self.create_item("Radish Seeds") summer = self.create_item("Summer") house = self.create_item("Progressive House") - self.multiworld.state.collect(seeds, prevent_sweep=False) - self.multiworld.state.collect(summer, prevent_sweep=False) - self.multiworld.state.collect(house, prevent_sweep=False) + self.multiworld.state.collect(seeds) + self.multiworld.state.collect(summer) + self.multiworld.state.collect(house) self.assert_rule_false(rule, self.multiworld.state) self.multiworld.state.remove(seeds) self.multiworld.state.remove(summer) self.multiworld.state.remove(house) - self.multiworld.state.collect(self.create_item("The Queen of Sauce"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("The Queen of Sauce")) self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py index 4719edea1d59..46a1b73d0b7a 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -25,7 +25,7 @@ def test_can_craft_recipe(self): self.collect_all_the_money() self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Marble Brazier Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Marble Brazier Recipe")) self.assert_rule_true(rule, self.multiworld.state) def test_can_learn_crafting_recipe(self): @@ -38,16 +38,16 @@ def test_can_learn_crafting_recipe(self): def test_can_craft_festival_recipe(self): recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Torch Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.multiworld.state.collect(self.create_item("Torch Recipe")) self.collect_lots_of_money() rule = self.world.logic.crafting.can_craft(recipe) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Fall")) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe")) self.assert_rule_true(rule, self.multiworld.state) def test_require_furnace_recipe_for_smelting_checks(self): @@ -64,7 +64,7 @@ def test_require_furnace_recipe_for_smelting_checks(self): self.collect_all_the_money() self.assert_rules_false(rules, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Furnace Recipe")) self.assert_rules_true(rules, self.multiworld.state) @@ -79,16 +79,16 @@ class TestCraftsanityWithFestivalsLogic(SVTestBase): def test_can_craft_festival_recipe(self): recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.multiworld.state.collect(self.create_item("Fall")) self.collect_lots_of_money() rule = self.world.logic.crafting.can_craft(recipe) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe")) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Torch Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Torch Recipe")) self.assert_rule_true(rule, self.multiworld.state) @@ -109,7 +109,7 @@ def test_can_craft_recipe(self): def test_can_craft_festival_recipe(self): recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) self.collect_lots_of_money() rule = self.world.logic.crafting.can_craft(recipe) result = rule(self.multiworld.state) @@ -126,7 +126,7 @@ def test_requires_mining_levels_for_smelting_checks(self): self.collect([self.create_item("Progressive Sword")] * 4) self.collect([self.create_item("Progressive Mine Elevator")] * 24) self.collect([self.create_item("Progressive Trash Can")] * 2) - self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Furnace Recipe")) self.collect([self.create_item("Combat Level")] * 10) self.collect([self.create_item("Fishing Level")] * 10) self.collect_all_the_money() @@ -147,11 +147,11 @@ class TestNoCraftsanityWithFestivalsLogic(SVTestBase): def test_can_craft_festival_recipe(self): recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.multiworld.state.collect(self.create_item("Fall")) self.collect_lots_of_money() rule = self.world.logic.crafting.can_craft(recipe) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe")) self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestDonations.py b/worlds/stardew_valley/test/rules/TestDonations.py index 984a3ebc38b4..3927bd09a48b 100644 --- a/worlds/stardew_valley/test/rules/TestDonations.py +++ b/worlds/stardew_valley/test/rules/TestDonations.py @@ -18,7 +18,7 @@ def test_cannot_make_any_donation_without_museum_access(self): for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(railroad_item)) for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -39,7 +39,7 @@ def test_cannot_make_any_donation_without_museum_access(self): for donation in donation_locations: self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(railroad_item)) for donation in donation_locations: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -58,7 +58,7 @@ def test_cannot_make_any_donation_without_museum_access(self): for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(railroad_item)) for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py index fb186ca99480..3e9109ed5010 100644 --- a/worlds/stardew_valley/test/rules/TestFriendship.py +++ b/worlds/stardew_valley/test/rules/TestFriendship.py @@ -11,34 +11,34 @@ class TestFriendsanityDatingRules(SVTestBase): def test_earning_dating_heart_requires_dating(self): self.collect_all_the_money() - self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Beach Bridge"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Fall")) + self.multiworld.state.collect(self.create_item("Beach Bridge")) + self.multiworld.state.collect(self.create_item("Progressive House")) for i in range(3): - self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive Weapon"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive Barn"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Weapon")) + self.multiworld.state.collect(self.create_item("Progressive Axe")) + self.multiworld.state.collect(self.create_item("Progressive Barn")) for i in range(10): - self.multiworld.state.collect(self.create_item("Foraging Level"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Farming Level"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Mining Level"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Combat Level"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Foraging Level")) + self.multiworld.state.collect(self.create_item("Farming Level")) + self.multiworld.state.collect(self.create_item("Mining Level")) + self.multiworld.state.collect(self.create_item("Combat Level")) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator")) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator")) npc = "Abigail" heart_name = f"{npc} <3" step = 3 self.assert_can_reach_heart_up_to(npc, 3, step) - self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(heart_name)) self.assert_can_reach_heart_up_to(npc, 6, step) - self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(heart_name)) self.assert_can_reach_heart_up_to(npc, 8, step) - self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(heart_name)) self.assert_can_reach_heart_up_to(npc, 10, step) - self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(heart_name)) self.assert_can_reach_heart_up_to(npc, 14, step) def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py index 973d8d3ada7d..b26d1e94ee2c 100644 --- a/worlds/stardew_valley/test/rules/TestShipping.py +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -76,7 +76,7 @@ def test_all_shipsanity_locations_require_shipping_bin(self): with self.subTest(location.name): self.remove(bin_item) self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) - self.multiworld.state.collect(bin_item, prevent_sweep=False) + self.multiworld.state.collect(bin_item) shipsanity_rule = self.world.logic.region.can_reach_location(location.name) self.assert_rule_true(shipsanity_rule, self.multiworld.state) self.remove(bin_item) diff --git a/worlds/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py index 7d10f4ceb1d3..49577d2223e0 100644 --- a/worlds/stardew_valley/test/rules/TestStateRules.py +++ b/worlds/stardew_valley/test/rules/TestStateRules.py @@ -1,12 +1,22 @@ -import unittest +from .. import SVTestBase, allsanity_mods_6_x_x +from ...stardew_rule import HasProgressionPercent -from BaseClasses import ItemClassification -from ...test import solo_multiworld +class TestHasProgressionPercentWithVictory(SVTestBase): + options = allsanity_mods_6_x_x() -class TestHasProgressionPercent(unittest.TestCase): - def test_max_item_amount_is_full_collection(self): - # Not caching because it fails too often for some reason - with solo_multiworld(world_caching=False) as (multiworld, world): - progression_item_count = sum(1 for i in multiworld.get_items() if i.classification & ItemClassification.progression) - self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory + def test_has_100_progression_percent_is_false_while_items_are_missing(self): + has_100_progression_percent = HasProgressionPercent(1, 100) + + for i, item in enumerate([i for i in self.multiworld.get_items() if i.advancement and i.code][1:]): + if item.name != "Victory": + self.collect(item) + self.assertFalse(has_100_progression_percent(self.multiworld.state), + f"Rule became true after {i} items, total_progression_items is {self.world.total_progression_items}") + + def test_has_100_progression_percent_account_for_victory_not_being_collected(self): + has_100_progression_percent = HasProgressionPercent(1, 100) + + self.collect_all_except("Victory") + + self.assert_rule_true(has_100_progression_percent, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py index 5f0fe8ef3ffb..5b8975f4e707 100644 --- a/worlds/stardew_valley/test/rules/TestTools.py +++ b/worlds/stardew_valley/test/rules/TestTools.py @@ -21,30 +21,30 @@ def test_sturgeon(self): self.assert_rule_false(sturgeon_rule, self.multiworld.state) summer = self.create_item("Summer") - self.multiworld.state.collect(summer, prevent_sweep=False) + self.multiworld.state.collect(summer) self.assert_rule_false(sturgeon_rule, self.multiworld.state) fishing_rod = self.create_item("Progressive Fishing Rod") - self.multiworld.state.collect(fishing_rod, prevent_sweep=False) - self.multiworld.state.collect(fishing_rod, prevent_sweep=False) + self.multiworld.state.collect(fishing_rod) + self.multiworld.state.collect(fishing_rod) self.assert_rule_false(sturgeon_rule, self.multiworld.state) fishing_level = self.create_item("Fishing Level") - self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level) self.assert_rule_false(sturgeon_rule, self.multiworld.state) - self.multiworld.state.collect(fishing_level, prevent_sweep=False) - self.multiworld.state.collect(fishing_level, prevent_sweep=False) - self.multiworld.state.collect(fishing_level, prevent_sweep=False) - self.multiworld.state.collect(fishing_level, prevent_sweep=False) - self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) self.assert_rule_true(sturgeon_rule, self.multiworld.state) self.remove(summer) self.assert_rule_false(sturgeon_rule, self.multiworld.state) winter = self.create_item("Winter") - self.multiworld.state.collect(winter, prevent_sweep=False) + self.multiworld.state.collect(winter) self.assert_rule_true(sturgeon_rule, self.multiworld.state) self.remove(fishing_rod) @@ -53,24 +53,24 @@ def test_sturgeon(self): def test_old_master_cannoli(self): self.multiworld.state.prog_items = {1: Counter()} - self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) - self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Axe")) + self.multiworld.state.collect(self.create_item("Progressive Axe")) + self.multiworld.state.collect(self.create_item("Summer")) self.collect_lots_of_money() rule = self.world.logic.region.can_reach_location("Old Master Cannoli") self.assert_rule_false(rule, self.multiworld.state) fall = self.create_item("Fall") - self.multiworld.state.collect(fall, prevent_sweep=False) + self.multiworld.state.collect(fall) self.assert_rule_false(rule, self.multiworld.state) tuesday = self.create_item("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, prevent_sweep=False) + self.multiworld.state.collect(tuesday) self.assert_rule_false(rule, self.multiworld.state) rare_seed = self.create_item("Rare Seed") - self.multiworld.state.collect(rare_seed, prevent_sweep=False) + self.multiworld.state.collect(rare_seed) self.assert_rule_true(rule, self.multiworld.state) self.remove(fall) @@ -80,11 +80,11 @@ def test_old_master_cannoli(self): green_house = self.create_item("Greenhouse") self.collect(self.create_item(Event.fall_farming)) - self.multiworld.state.collect(green_house, prevent_sweep=False) + self.multiworld.state.collect(green_house) self.assert_rule_false(rule, self.multiworld.state) friday = self.create_item("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, prevent_sweep=False) + self.multiworld.state.collect(friday) self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) self.remove(green_house) @@ -111,7 +111,7 @@ def test_cannot_get_any_tool_without_blacksmith_access(self): for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(railroad_item)) for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: @@ -125,7 +125,7 @@ def test_cannot_get_fishing_rod_without_willy_access(self): for fishing_rod_level in [3, 4]: self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) + self.multiworld.state.collect(self.create_item(railroad_item)) for fishing_rod_level in [3, 4]: self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py index 972170b93c75..383f26e841d2 100644 --- a/worlds/stardew_valley/test/rules/TestWeapons.py +++ b/worlds/stardew_valley/test/rules/TestWeapons.py @@ -10,40 +10,40 @@ class TestWeaponsLogic(SVTestBase): } def test_mine(self): - self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) - self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) - self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) - self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) - self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive House")) self.collect([self.create_item("Combat Level")] * 10) self.collect([self.create_item("Mining Level")] * 10) self.collect([self.create_item("Progressive Mine Elevator")] * 24) - self.multiworld.state.collect(self.create_item("Bus Repair"), prevent_sweep=True) - self.multiworld.state.collect(self.create_item("Skull Key"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Bus Repair")) + self.multiworld.state.collect(self.create_item("Skull Key")) - self.GiveItemAndCheckReachableMine("Progressive Sword", 1) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) - self.GiveItemAndCheckReachableMine("Progressive Club", 1) + self.give_item_and_check_reachable_mine("Progressive Sword", 1) + self.give_item_and_check_reachable_mine("Progressive Dagger", 1) + self.give_item_and_check_reachable_mine("Progressive Club", 1) - self.GiveItemAndCheckReachableMine("Progressive Sword", 2) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 2) - self.GiveItemAndCheckReachableMine("Progressive Club", 2) + self.give_item_and_check_reachable_mine("Progressive Sword", 2) + self.give_item_and_check_reachable_mine("Progressive Dagger", 2) + self.give_item_and_check_reachable_mine("Progressive Club", 2) - self.GiveItemAndCheckReachableMine("Progressive Sword", 3) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 3) - self.GiveItemAndCheckReachableMine("Progressive Club", 3) + self.give_item_and_check_reachable_mine("Progressive Sword", 3) + self.give_item_and_check_reachable_mine("Progressive Dagger", 3) + self.give_item_and_check_reachable_mine("Progressive Club", 3) - self.GiveItemAndCheckReachableMine("Progressive Sword", 4) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 4) - self.GiveItemAndCheckReachableMine("Progressive Club", 4) + self.give_item_and_check_reachable_mine("Progressive Sword", 4) + self.give_item_and_check_reachable_mine("Progressive Dagger", 4) + self.give_item_and_check_reachable_mine("Progressive Club", 4) - self.GiveItemAndCheckReachableMine("Progressive Sword", 5) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 5) - self.GiveItemAndCheckReachableMine("Progressive Club", 5) + self.give_item_and_check_reachable_mine("Progressive Sword", 5) + self.give_item_and_check_reachable_mine("Progressive Dagger", 5) + self.give_item_and_check_reachable_mine("Progressive Club", 5) - def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): + def give_item_and_check_reachable_mine(self, item_name: str, reachable_level: int): item = self.multiworld.create_item(item_name, self.player) - self.multiworld.state.collect(item, prevent_sweep=True) + self.multiworld.state.collect(item) rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() if reachable_level > 0: self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 137a7172aff4..b4d0f30ea51f 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -7,9 +7,6 @@ from BaseClasses import get_seed from .. import SVTestCase -# There seems to be 4 bytes that appear at random at the end of the output, breaking the json... I don't know where they came from. -BYTES_TO_REMOVE = 4 - # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") @@ -27,8 +24,8 @@ def test_all_locations_and_items_are_the_same_between_two_generations(self): output_a = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) output_b = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) - result_a = json.loads(output_a[:-BYTES_TO_REMOVE]) - result_b = json.loads(output_b[:-BYTES_TO_REMOVE]) + result_a = json.loads(output_a) + result_b = json.loads(output_b) for i, ((room_a, bundles_a), (room_b, bundles_b)) in enumerate(zip(result_a["bundles"].items(), result_b["bundles"].items())): self.assertEqual(room_a, room_b, f"Bundle rooms at index {i} is different between both executions. Seed={seed}") From ad30e3264aedb9eb02cee167cc5d487594acd5c5 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:15:50 +0100 Subject: [PATCH 048/112] The Witness: Turn off default tests on a test that is prone to swap fails #4261 --- worlds/witness/test/test_disable_non_randomized.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py index e7cb1597b2ba..bf285f035d5b 100644 --- a/worlds/witness/test/test_disable_non_randomized.py +++ b/worlds/witness/test/test_disable_non_randomized.py @@ -3,6 +3,8 @@ class TestDisableNonRandomized(WitnessTestBase): + run_default_tests = False + options = { "disable_non_randomized_puzzles": True, "shuffle_doors": "panels", From 089b3f17a7058d281624828a747ecb3ccba071cf Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:16:52 +0100 Subject: [PATCH 049/112] The Witness: Add "Panel Keys" and "Obelisk Keys" item groups #4026 --- worlds/witness/data/static_items.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index e5103ef3807e..c64df741982e 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Set +from typing import Dict, List, Set, cast from BaseClasses import ItemClassification @@ -41,7 +41,19 @@ def populate_items() -> None: ITEM_GROUPS.setdefault("Symbols", set()).add(item_name) elif definition.category is ItemCategory.DOOR: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Doors", set()).add(item_name) + + first_entity_hex = cast(DoorItemDefinition, definition).panel_id_hexes[0] + entity_type = static_witness_logic.ENTITIES_BY_HEX[first_entity_hex]["entityType"] + + if entity_type == "Door": + ITEM_GROUPS.setdefault("Doors", set()).add(item_name) + elif entity_type == "Panel": + ITEM_GROUPS.setdefault("Panel Keys", set()).add(item_name) + elif entity_type in {"EP", "Obelisk Side", "Obelisk"}: + ITEM_GROUPS.setdefault("Obelisk Keys", set()).add(item_name) + else: + raise ValueError(f"Couldn't figure out what type of door item {definition} is.") + elif definition.category is ItemCategory.LASER: classification = ItemClassification.progression_skip_balancing ITEM_GROUPS.setdefault("Lasers", set()).add(item_name) From 72e88bb493b465ad2cfb1c4250c9bed60fb220fa Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:36:00 -0500 Subject: [PATCH 050/112] SMZ3: generate without rom (#3461) * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * now doesnt require ROM for generation * removed stage_assert_generate * fixed conflict with main and small cleanup --- worlds/smz3/Rom.py | 24 ++++++++++++++++++++++-- worlds/smz3/__init__.py | 37 ++++++++++--------------------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/worlds/smz3/Rom.py b/worlds/smz3/Rom.py index 3fec151dc679..d66d9239792d 100644 --- a/worlds/smz3/Rom.py +++ b/worlds/smz3/Rom.py @@ -3,18 +3,38 @@ import Utils from Utils import read_snes_rom -from worlds.Files import APDeltaPatch +from worlds.Files import APProcedurePatch, APPatchExtension, APTokenMixin, APTokenTypes +from worlds.smz3.ips import IPS_Patch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' LTTPJPN10HASH = '03a63945398191337e896e5771f77173' ROM_PLAYER_LIMIT = 256 +world_folder = os.path.dirname(__file__) -class SMZ3DeltaPatch(APDeltaPatch): +class SMZ3PatchExtensions(APPatchExtension): + game = "SMZ3" + + @staticmethod + def apply_basepatch(caller: APProcedurePatch, rom: bytes) -> bytes: + basepatch = IPS_Patch.load(world_folder + "/data/zsm.ips") + return basepatch.apply(rom) + +class SMZ3ProcedurePatch(APProcedurePatch, APTokenMixin): hash = "3a177ba9879e3dd04fb623a219d175b2" game = "SMZ3" patch_file_ending = ".apsmz3" + procedure = [ + ("apply_basepatch", []), + ("apply_tokens", ["token_data.bin"]), + ] + + def write_tokens(self, patches): + for addr, data in patches.items(): + self.write_token(APTokenTypes.WRITE, addr, bytes(data)) + self.write_file("token_data.bin", self.get_token_binary()) + @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 5e6a6ac60965..838db1f7e745 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -19,11 +19,10 @@ from .TotalSMZ3.Region import IReward, IMedallionAccess from .TotalSMZ3.Text.Texts import openFile from worlds.AutoWorld import World, AutoLogicRegister, WebWorld -from .Client import SMZ3SNIClient -from .Rom import get_base_rom_bytes, SMZ3DeltaPatch -from .ips import IPS_Patch +from .Rom import SMZ3ProcedurePatch from .Options import SMZ3Options -from Options import Accessibility, ItemsAccessibility +from Options import ItemsAccessibility +from .Client import SMZ3SNIClient world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -183,10 +182,6 @@ def isProgression(cls, itemType): } return itemType in progressionTypes - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld): - base_combined_rom = get_base_rom_bytes() - def generate_early(self): self.config = Config() self.config.GameMode = GameMode.Multiworld @@ -444,10 +439,6 @@ def apply_customization(self): def generate_output(self, output_directory: str): try: - base_combined_rom = get_base_rom_bytes() - basepatch = IPS_Patch.load(world_folder + "/data/zsm.ips") - base_combined_rom = basepatch.apply(base_combined_rom) - patcher = TotalSMZ3Patch(self.smz3World, [world.smz3World for key, world in self.multiworld.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")], self.multiworld.seed_name, @@ -459,21 +450,13 @@ def generate_output(self, output_directory: str): patches.update(self.apply_sm_custom_sprite()) patches.update(self.apply_item_names()) patches.update(self.apply_customization()) - for addr, bytes in patches.items(): - offset = 0 - for byte in bytes: - base_combined_rom[addr + offset] = byte - offset += 1 - - outfilebase = self.multiworld.get_out_file_name_base(self.player) - - filename = os.path.join(output_directory, f"{outfilebase}.sfc") - with open(filename, "wb") as binary_file: - binary_file.write(base_combined_rom) - patch = SMZ3DeltaPatch(os.path.splitext(filename)[0] + SMZ3DeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=filename) - patch.write() - os.remove(filename) + + patch = SMZ3ProcedurePatch(player=self.player, player_name=self.player_name) + patch.write_tokens(patches) + rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" + f"{patch.patch_file_ending}") + patch.write(rom_path) + self.rom_name = bytearray(patcher.title, 'utf8') except: raise From 7adb673a80a4fcaf8c5ca87369c5851fe2d7a0bf Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 30 Nov 2024 03:37:08 +0100 Subject: [PATCH 051/112] Core: "Fix" Priority Fill (#3592) * Priority fill -> Don't use one item per player * fix unit test thing * Ok, I think this should do it properly --- Fill.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Fill.py b/Fill.py index 912b4d05bed9..86a4639c51ce 100644 --- a/Fill.py +++ b/Fill.py @@ -36,7 +36,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location], item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: + allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True, + name: str = "Unknown") -> None: """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. @@ -63,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati placed = 0 while any(reachable_items.values()) and locations: - # grab one item per player - items_to_place = [items.pop() - for items in reachable_items.values() if items] + if one_item_per_player: + # grab one item per player + items_to_place = [items.pop() + for items in reachable_items.values() if items] + else: + next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items]) + items_to_place = [] + if item_pool: + items_to_place.append(reachable_items[next_player].pop()) + for item in items_to_place: for p, pool_item in enumerate(item_pool): if pool_item is item: item_pool.pop(p) break + maximum_exploration_state = sweep_from_pool( base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player) if single_player_placement else None) @@ -480,7 +489,8 @@ def mark_for_locking(location: Location): if prioritylocations: # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, - single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority") + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority", one_item_per_player=False) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations From 845a60495589e7b25a8d3f225c5aac40cb153826 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 29 Nov 2024 21:40:14 -0500 Subject: [PATCH 052/112] MultiServer: !status shows Ready status (#3598) * Makes !status show a note if the slot is in Status Ready * update variable name for better clarity --- MultiServer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 0db8722b5cb6..628817d5977c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -975,9 +975,13 @@ def get_status_string(ctx: Context, team: int, tag: str): tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" - goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." + status_text = ( + " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else + " and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else + "." + ) text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{tag_text}{goal_text} {completion_text}" + f"{tag_text}{status_text} {completion_text}" return text From a537d8eb65a0b85210504691ef420cfdb8d7f3b3 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 29 Nov 2024 21:58:52 -0500 Subject: [PATCH 053/112] Launcher: support Component icons inside apworlds (#3629) * Add kivy overrides to allow AsyncImage source paths of the format ap:worlds.module/subpath/to/data.png that use pkgutil to load files from within an apworld * Apply suggestions from code review Co-authored-by: Doug Hoskisson * Apply suggestions from code review Co-authored-by: Doug Hoskisson * change original-load variable name for clarity per review * add comment to record pkgutil format * remove dependency on PIL * i hate typing --------- Co-authored-by: Doug Hoskisson --- Launcher.py | 7 +++---- kvui.py | 38 ++++++++++++++++++++++++++++++++++++ worlds/LauncherComponents.py | 1 + 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Launcher.py b/Launcher.py index f04d67a5aa0d..0b8be232170d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -246,9 +246,8 @@ def launch(exe, in_terminal=False): def run_gui(): - from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget + from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage from kivy.core.window import Window - from kivy.uix.image import AsyncImage from kivy.uix.relativelayout import RelativeLayout class Launcher(App): @@ -281,8 +280,8 @@ def build_button(component: Component) -> Widget: button.component = component button.bind(on_release=self.component_action) if component.icon != "icon": - image = AsyncImage(source=icon_paths[component.icon], - size=(38, 38), size_hint=(None, 1), pos=(5, 0)) + image = ApAsyncImage(source=icon_paths[component.icon], + size=(38, 38), size_hint=(None, 1), pos=(5, 0)) box_layout = RelativeLayout(size_hint_y=None, height=40) box_layout.add_widget(button) box_layout.add_widget(image) diff --git a/kvui.py b/kvui.py index dfe935930049..d98fc7ed9ab8 100644 --- a/kvui.py +++ b/kvui.py @@ -3,6 +3,8 @@ import sys import typing import re +import io +import pkgutil from collections import deque assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility" @@ -34,6 +36,7 @@ from kivy.core.window import Window from kivy.core.clipboard import Clipboard from kivy.core.text.markup import MarkupLabel +from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData from kivy.base import ExceptionHandler, ExceptionManager from kivy.clock import Clock from kivy.factory import Factory @@ -61,6 +64,7 @@ from kivy.uix.recycleview.layout import LayoutSelectionBehavior from kivy.animation import Animation from kivy.uix.popup import Popup +from kivy.uix.image import AsyncImage fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) @@ -838,6 +842,40 @@ def fix_heights(self): element.height = max_height +class ApAsyncImage(AsyncImage): + def is_uri(self, filename: str) -> bool: + if filename.startswith("ap:"): + return True + else: + return super().is_uri(filename) + + +class ImageLoaderPkgutil(ImageLoaderBase): + def load(self, filename: str) -> typing.List[ImageData]: + # take off the "ap:" prefix + module, path = filename[3:].split("/", 1) + data = pkgutil.get_data(module, path) + return self._bytes_to_data(data) + + def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]: + loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory()) + return loader.load(loader, io.BytesIO(data)) + + +# grab the default loader method so we can override it but use it as a fallback +_original_image_loader_load = ImageLoader.load + + +def load_override(filename: str, default_load=_original_image_loader_load, **kwargs): + if filename.startswith("ap:"): + return ImageLoaderPkgutil(filename) + else: + return default_load(filename, **kwargs) + + +ImageLoader.load = load_override + + class E(ExceptionHandler): logger = logging.getLogger("Client") diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 67806a7394c7..7f178f1739fc 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -207,6 +207,7 @@ def install_apworld(apworld_path: str = "") -> None: ] +# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used icon_paths = { 'icon': local_path('data', 'icon.png'), 'mcicon': local_path('data', 'mcicon.png'), From 0705f6e6c06baa33d60573e0a201385ba6e27ca4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 30 Nov 2024 04:08:17 +0100 Subject: [PATCH 054/112] Factorio: option groups (#4293) --- worlds/factorio/Options.py | 41 ++++++++++++++++++++++++++++++++----- worlds/factorio/__init__.py | 4 +++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 5a41250fa760..dd17e2d68bdd 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -6,7 +6,7 @@ from schema import Schema, Optional, And, Or from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ - StartInventoryPool, PerGameCommonOptions + StartInventoryPool, PerGameCommonOptions, OptionGroup # schema helpers FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) @@ -293,7 +293,7 @@ class FactorioWorldGen(OptionDict): with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" display_name = "World Generation" # FIXME: do we want default be a rando-optimized default or in-game DS? - value: typing.Dict[str, typing.Dict[str, typing.Any]] + value: dict[str, dict[str, typing.Any]] default = { "autoplace_controls": { # terrain @@ -402,7 +402,7 @@ class FactorioWorldGen(OptionDict): } }) - def __init__(self, value: typing.Dict[str, typing.Any]): + def __init__(self, value: dict[str, typing.Any]): advanced = {"pollution", "enemy_evolution", "enemy_expansion"} self.value = { "basic": {k: v for k, v in value.items() if k not in advanced}, @@ -421,7 +421,7 @@ def optional_min_lte_max(container, min_key, max_key): optional_min_lte_max(enemy_expansion, "min_expansion_cooldown", "max_expansion_cooldown") @classmethod - def from_any(cls, data: typing.Dict[str, typing.Any]) -> FactorioWorldGen: + def from_any(cls, data: dict[str, typing.Any]) -> FactorioWorldGen: if type(data) == dict: return cls(data) else: @@ -435,7 +435,7 @@ class ImportedBlueprint(DefaultOnToggle): class EnergyLink(Toggle): """Allow sending energy to other worlds. 25% of the energy is lost in the transfer.""" - display_name = "EnergyLink" + display_name = "Energy Link" @dataclass @@ -473,3 +473,34 @@ class FactorioOptions(PerGameCommonOptions): death_link: DeathLink energy_link: EnergyLink start_inventory_from_pool: StartInventoryPool + + +option_groups: list[OptionGroup] = [ + OptionGroup( + "Technologies", + [ + TechTreeLayout, + Progressive, + MinTechCost, + MaxTechCost, + TechCostDistribution, + TechCostMix, + RampingTechCosts, + TechTreeInformation, + ] + ), + OptionGroup( + "Traps", + [ + AttackTrapCount, + EvolutionTrapCount, + EvolutionTrapIncrease, + TeleportTrapCount, + GrenadeTrapCount, + ClusterGrenadeTrapCount, + ArtilleryTrapCount, + AtomicRocketTrapCount, + ], + start_collapsed=True + ), +] diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 9f1f3cb573f9..dd14ab434717 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -12,7 +12,8 @@ from worlds.generic import Rules from .Locations import location_pools, location_table from .Mod import generate_mod -from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution +from .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, + TechCostDistribution, option_groups) from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_product_sources, required_technologies, get_rocket_requirements, \ @@ -61,6 +62,7 @@ class FactorioWeb(WebWorld): "setup/en", ["Berserker, Farrak Kilhn"] )] + option_groups = option_groups class FactorioItem(Item): From c1b27f79ac4107e5b253f8cf68fcbfb64d9a11f8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 30 Nov 2024 04:11:03 +0100 Subject: [PATCH 055/112] Core: cull events from multidata spheres (#3623) Co-authored-by: Doug Hoskisson --- BaseClasses.py | 43 +++++++++++++++++++++++++++++++++++++++++++ Main.py | 5 ++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d84da5d212b4..98ada4f861ec 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -604,6 +604,49 @@ def get_spheres(self) -> Iterator[Set[Location]]: state.collect(location.item, True, location) locations -= sphere + def get_sendable_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere + + If there are unreachable locations, the last sphere of reachable locations is followed by an empty set, + and then a set of all of the unreachable locations. + """ + state = CollectionState(self) + locations: Set[Location] = set() + events: Set[Location] = set() + for location in self.get_filled_locations(): + if type(location.item.code) is int: + locations.add(location) + else: + events.add(location) + + while locations: + sphere: Set[Location] = set() + + # cull events out + done_events: Set[Union[Location, None]] = {None} + while done_events: + done_events = set() + for event in events: + if event.can_reach(state): + state.collect(event.item, True, event) + done_events.add(event) + events -= done_events + + for location in locations: + if location.can_reach(state): + sphere.add(location) + + yield sphere + if not sphere: + if locations: + yield locations # unreachable locations + break + + for location in sphere: + state.collect(location.item, True, location) + locations -= sphere + def fulfills_accessibility(self, state: Optional[CollectionState] = None): """Check if accessibility rules are fulfilled with current or supplied state.""" if not state: diff --git a/Main.py b/Main.py index b9612e9549ba..3a11181bd99f 100644 --- a/Main.py +++ b/Main.py @@ -306,11 +306,10 @@ def precollect_hint(location): # get spheres -> filter address==None -> skip empty spheres: List[Dict[int, Set[int]]] = [] - for sphere in multiworld.get_spheres(): + for sphere in multiworld.get_sendable_spheres(): current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) for sphere_location in sphere: - if type(sphere_location.address) is int: - current_sphere[sphere_location.player].add(sphere_location.address) + current_sphere[sphere_location.player].add(sphere_location.address) if current_sphere: spheres.append(dict(current_sphere)) From 9eaca9527783d88e176119e7b2f436a374159357 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 30 Nov 2024 04:11:28 +0100 Subject: [PATCH 056/112] WebHost: add a page to manage session cookie (#4173) --- WebHostLib/__init__.py | 2 +- WebHostLib/misc.py | 7 ------- WebHostLib/session.py | 31 +++++++++++++++++++++++++++++++ WebHostLib/templates/session.html | 30 ++++++++++++++++++++++++++++++ WebHostLib/templates/siteMap.html | 1 + 5 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 WebHostLib/session.py create mode 100644 WebHostLib/templates/session.html diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index dbe2182b0747..9b2b6736f13c 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -85,6 +85,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, options + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index c49b1ae17801..6be0e470b3b4 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -18,13 +18,6 @@ def get_world_theme(game_name: str): return 'grass' -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - @app.errorhandler(404) @app.errorhandler(jinja2.exceptions.TemplateNotFound) def page_not_found(err): diff --git a/WebHostLib/session.py b/WebHostLib/session.py new file mode 100644 index 000000000000..d5dab7d6e6e6 --- /dev/null +++ b/WebHostLib/session.py @@ -0,0 +1,31 @@ +from uuid import uuid4, UUID + +from flask import session, render_template + +from WebHostLib import app + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.route('/session') +def show_session(): + return render_template( + "session.html", + ) + + +@app.route('/session/') +def set_session(_id: str): + new_id: UUID = UUID(_id, version=4) + old_id: UUID = session["_id"] + if old_id != new_id: + session["_id"] = new_id + return render_template( + "session.html", + old_id=old_id, + ) diff --git a/WebHostLib/templates/session.html b/WebHostLib/templates/session.html new file mode 100644 index 000000000000..b75474483a8f --- /dev/null +++ b/WebHostLib/templates/session.html @@ -0,0 +1,30 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {% include 'header/stoneHeader.html' %} + Session + +{% endblock %} + +{% block body %} +
+{% endblock %} diff --git a/WebHostLib/templates/siteMap.html b/WebHostLib/templates/siteMap.html index cdd6ad45eb27..b7db8227dc50 100644 --- a/WebHostLib/templates/siteMap.html +++ b/WebHostLib/templates/siteMap.html @@ -26,6 +26,7 @@

Base Pages

  • User Content
  • Game Statistics
  • Glossary
  • +
  • Session / Login
  • Tutorials

    From d6da3bc899cd11e76a5a6da4fb6870134341d080 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 30 Nov 2024 06:53:28 +0100 Subject: [PATCH 057/112] Factorio: add Atomic Cliff Remover Trap (#4282) --- worlds/factorio/Options.py | 8 +++++++ worlds/factorio/__init__.py | 5 ++++- worlds/factorio/data/mod/lib.lua | 21 ++++++++++++++----- worlds/factorio/data/mod_template/control.lua | 7 +++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index dd17e2d68bdd..72f438778b60 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -272,6 +272,12 @@ class AtomicRocketTrapCount(TrapCount): display_name = "Atomic Rocket Traps" +class AtomicCliffRemoverTrapCount(TrapCount): + """Trap items that when received trigger an atomic rocket explosion on a random cliff. + Warning: there is no warning. The launch is instantaneous.""" + display_name = "Atomic Cliff Remover Traps" + + class EvolutionTrapCount(TrapCount): """Trap items that when received increase the enemy evolution.""" display_name = "Evolution Traps" @@ -467,6 +473,7 @@ class FactorioOptions(PerGameCommonOptions): cluster_grenade_traps: ClusterGrenadeTrapCount artillery_traps: ArtilleryTrapCount atomic_rocket_traps: AtomicRocketTrapCount + atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount attack_traps: AttackTrapCount evolution_traps: EvolutionTrapCount evolution_trap_increase: EvolutionTrapIncrease @@ -500,6 +507,7 @@ class FactorioOptions(PerGameCommonOptions): ClusterGrenadeTrapCount, ArtilleryTrapCount, AtomicRocketTrapCount, + AtomicCliffRemoverTrapCount, ], start_collapsed=True ), diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index dd14ab434717..8f8abeb292f1 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -77,6 +77,7 @@ class FactorioItem(Item): all_items["Cluster Grenade Trap"] = factorio_base_id - 5 all_items["Artillery Trap"] = factorio_base_id - 6 all_items["Atomic Rocket Trap"] = factorio_base_id - 7 +all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8 class Factorio(World): @@ -142,6 +143,7 @@ def create_regions(self): self.options.grenade_traps + \ self.options.cluster_grenade_traps + \ self.options.atomic_rocket_traps + \ + self.options.atomic_cliff_remover_traps + \ self.options.artillery_traps location_pool = [] @@ -194,7 +196,8 @@ def sorter(loc: FactorioScienceLocation): def create_items(self) -> None: self.custom_technologies = self.set_custom_technologies() self.set_custom_recipes() - traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") + traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket", + "Atomic Cliff Remover") for trap_name in traps: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in range(getattr(self.options, diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 7be7403e48f1..517a54e3d642 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -28,12 +28,23 @@ function random_offset_position(position, offset) end function fire_entity_at_players(entity_name, speed) + local entities = {} for _, player in ipairs(game.forces["player"].players) do - current_character = player.character - if current_character ~= nil then - current_character.surface.create_entity{name=entity_name, - position=random_offset_position(current_character.position, 128), - target=current_character, speed=speed} + if player.character ~= nil then + table.insert(entities, player.character) end end + return fire_entity_at_entities(entity_name, entities, speed) +end + +function fire_entity_at_entities(entity_name, entities, speed) + for _, current_entity in ipairs(entities) do + local target = current_entity + if target.health == nil then + target = target.position + end + current_entity.surface.create_entity{name=entity_name, + position=random_offset_position(current_entity.position, 128), + target=target, speed=speed} + end end diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index b08608a60ae9..e486c7433095 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -737,6 +737,13 @@ end, ["Atomic Rocket Trap"] = function () fire_entity_at_players("atomic-rocket", 0.1) end, +["Atomic Cliff Remover Trap"] = function () + local cliffs = game.surfaces["nauvis"].find_entities_filtered{type = "cliff"} + + if #cliffs > 0 then + fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1) + end +end, } commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) From 7cbd50a2e6f646870f0309a9f8c93bfe24f3a18a Mon Sep 17 00:00:00 2001 From: qwint Date: Sat, 30 Nov 2024 10:02:32 -0500 Subject: [PATCH 058/112] HK: add item group for dream nail(s) (#4069) --- worlds/hk/Items.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py index 8515465826a5..a2b7c06d62a6 100644 --- a/worlds/hk/Items.py +++ b/worlds/hk/Items.py @@ -61,6 +61,7 @@ class HKItemData(NamedTuple): "VesselFragments": lookup_type_to_names["Vessel"], "WhisperingRoots": lookup_type_to_names["Root"], "WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"}, + "DreamNails": {"Dream_Nail", "Dream_Gate", "Awoken_Dream_Nail"}, }) item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash'] item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'} From ca6792a8a7c5f82e3a8c539c7f0d23e44b181c4f Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 30 Nov 2024 10:08:41 -0500 Subject: [PATCH 059/112] Blasphemous: Add start_inventory_from_pool (#4217) --- worlds/blasphemous/Options.py | 3 ++- worlds/blasphemous/__init__.py | 9 --------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index 0bd08b13b260..e0bbcd770758 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup +from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool, OptionGroup import random @@ -213,6 +213,7 @@ class BlasphemousDeathLink(DeathLink): @dataclass class BlasphemousOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool prie_dieu_warp: PrieDieuWarp skip_cutscenes: SkipCutscenes corpse_hints: CorpseHints diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 67031710e4eb..a967fbac9289 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -137,12 +137,6 @@ def create_items(self): ] skipped_items = [] - junk: int = 0 - - for item, count in self.options.start_inventory.value.items(): - for _ in range(count): - skipped_items.append(item) - junk += 1 skipped_items.extend(unrandomized_dict.values()) @@ -194,9 +188,6 @@ def create_items(self): for _ in range(count): pool.append(self.create_item(item["name"])) - for _ in range(junk): - pool.append(self.create_item(self.get_filler_item_name())) - self.multiworld.itempool += pool self.place_items_from_dict(unrandomized_dict) From b83b48629de128a0b04c71b40ea4dc8b9aafc6e8 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 30 Nov 2024 17:23:28 +0100 Subject: [PATCH 060/112] Core: rework python version check (#4294) * Docs: update min required version and add comment about security. * Core: rework python version check * CI: set min micro update for build and release --- .github/workflows/build.yml | 6 ++++-- .github/workflows/release.yml | 3 ++- ModuleUpdate.py | 11 +++++++++-- docs/running from source.md | 4 +++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab94326d8188..27ca76e41f8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,8 @@ jobs: - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '~3.12.7' + check-latest: true - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip @@ -111,7 +112,8 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | echo "PYTHON=python3.12" >> $GITHUB_ENV diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b28ec8733408..aec4f90998cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,8 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | echo "PYTHON=python3.12" >> $GITHUB_ENV diff --git a/ModuleUpdate.py b/ModuleUpdate.py index dada16cefcaf..04cf25ea5594 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -5,8 +5,15 @@ import warnings -if sys.version_info < (3, 10, 11): - raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.") +if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11): + # Official micro version updates. This should match the number in docs/running from source.md. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.") +elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15): + # There are known security issues, but no easy way to install fixed versions on Windows for testing. + warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.") +elif sys.version_info < (3, 10, 1): + # Other platforms may get security backports instead of micro updates, so the number is unreliable. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) diff --git a/docs/running from source.md b/docs/running from source.md index 66dd1925c897..33d6b3928e54 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -7,7 +7,9 @@ use that version. These steps are for developers or platforms without compiled r ## General What you'll need: - * [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version + * [Python 3.10.11 or newer](https://www.python.org/downloads/), not the Windows Store version + * On Windows, please consider only using the latest supported version in production environments since security + updates for older versions are not easily available. * Python 3.12.x is currently the newest supported version * pip: included in downloads from python.org, separate in many Linux distributions * Matching C compiler From e5374eb8b81ac9913f4618f660d6f3c65b63261a Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Sun, 1 Dec 2024 03:22:02 +0100 Subject: [PATCH 061/112] [PKMN RB] Make Encounters in one location unique (#3994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Makes encounters in a location generate unique Pokémon * vyneras actually got it to work * V5 Update Fix Part 1 * Part 2 * final puzzle piece --- worlds/pokemon_rb/encounters.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py index fbe4abfe4466..aa20114787c3 100644 --- a/worlds/pokemon_rb/encounters.py +++ b/worlds/pokemon_rb/encounters.py @@ -170,6 +170,8 @@ def process_pokemon_locations(self): encounter_slots = encounter_slots_master.copy() zone_mapping = {} + zone_placed_mons = {} + if self.options.randomize_wild_pokemon: mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3] @@ -180,11 +182,13 @@ def process_pokemon_locations(self): zone = " - ".join(location.name.split(" - ")[:-1]) if zone not in zone_mapping: zone_mapping[zone] = {} + if zone not in zone_placed_mons: + zone_placed_mons[zone] = [] original_mon = slot.original_item if self.options.area_1_to_1_mapping and original_mon in zone_mapping[zone]: mon = zone_mapping[zone][original_mon] else: - mon = randomize_pokemon(self, original_mon, mons_list, + mon = randomize_pokemon(self, original_mon, [m for m in mons_list if m not in zone_placed_mons[zone]], self.options.randomize_wild_pokemon.value, self.random) # while ("Pokemon Tower 6F" in slot.name and @@ -201,6 +205,7 @@ def process_pokemon_locations(self): location.item.location = location locations.append(location) zone_mapping[zone][original_mon] = mon + zone_placed_mons[zone].append(mon) mons_to_add = [] remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and @@ -270,4 +275,4 @@ def process_pokemon_locations(self): location.item = self.create_item(slot.original_item) location.locked = True location.item.location = location - placed_mons[location.item.name] += 1 \ No newline at end of file + placed_mons[location.item.name] += 1 From f735416bdac39db987ffb8af4b0d7bc09846c172 Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Sat, 30 Nov 2024 18:46:34 -0800 Subject: [PATCH 062/112] id Tech 1: Clean up difficulty options (#4298) --- worlds/doom_1993/Options.py | 11 ++++++++--- worlds/doom_ii/Options.py | 11 ++++++++--- worlds/heretic/Options.py | 15 +++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/worlds/doom_1993/Options.py b/worlds/doom_1993/Options.py index b4fc50aac674..c9c61110328c 100644 --- a/worlds/doom_1993/Options.py +++ b/worlds/doom_1993/Options.py @@ -16,9 +16,9 @@ class Goal(Choice): class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. + Choose the game difficulty. These options match DOOM's skill levels. + baby (I'm too young to die.) Same as easy, with double ammo pickups and half damage taken. + easy (Hey, not too rough.) Less monsters or strength. medium (Hurt me plenty.) Default. hard (Ultra-Violence.) More monsters or strength. nightmare (Nightmare!) Monsters attack more rapidly and respawn. @@ -29,6 +29,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_nightmare = 4 + alias_itytd = 0 + alias_hntr = 1 + alias_hmp = 2 + alias_uv = 3 + alias_nm = 4 default = 2 diff --git a/worlds/doom_ii/Options.py b/worlds/doom_ii/Options.py index 6ecfb1aecd6c..98c8ebc56e16 100644 --- a/worlds/doom_ii/Options.py +++ b/worlds/doom_ii/Options.py @@ -6,9 +6,9 @@ class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. + Choose the game difficulty. These options match DOOM's skill levels. + baby (I'm too young to die.) Same as easy, with double ammo pickups and half damage taken. + easy (Hey, not too rough.) Less monsters or strength. medium (Hurt me plenty.) Default. hard (Ultra-Violence.) More monsters or strength. nightmare (Nightmare!) Monsters attack more rapidly and respawn. @@ -19,6 +19,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_nightmare = 4 + alias_itytd = 0 + alias_hntr = 1 + alias_hmp = 2 + alias_uv = 3 + alias_nm = 4 default = 2 diff --git a/worlds/heretic/Options.py b/worlds/heretic/Options.py index 14f4198a55f0..7d98207b0f8e 100644 --- a/worlds/heretic/Options.py +++ b/worlds/heretic/Options.py @@ -16,14 +16,8 @@ class Goal(Choice): class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. - medium (Hurt me plenty.) Default. - hard (Ultra-Violence.) More monsters or strength. - nightmare (Nightmare!) Monsters attack more rapidly and respawn. - - wet nurse (hou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death. + Choose the game difficulty. These options match Heretic's skill levels. + wet nurse (Thou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death. easy (Yellowbellies-r-us) - Fewer monsters and more items than medium. medium (Bringest them oneth) - Completely balanced, this is the standard difficulty level. hard (Thou art a smite-meister) - More monsters and fewer items than medium. @@ -35,6 +29,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_black_plague = 4 + alias_wn = 0 + alias_yru = 1 + alias_bto = 2 + alias_sm = 3 + alias_bp = 4 default = 2 From a67688749f556c631b3e1423c03f64d6151a5cb4 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sat, 30 Nov 2024 21:52:07 -0500 Subject: [PATCH 063/112] Stardew Valley: Refactor skill progression to use new feature system (#3662) * create a first draft of the feature * use feature in items and locations * add content to more places * use feature in logic * replace option check by feature * remove unused code * remove weird white space * some import nitpicking * flip negative if --- worlds/stardew_valley/__init__.py | 4 +- worlds/stardew_valley/bundles/bundle_item.py | 24 +++--- worlds/stardew_valley/content/__init__.py | 21 ++++- .../content/feature/__init__.py | 1 + .../content/feature/skill_progression.py | 46 +++++++++++ worlds/stardew_valley/content/game_content.py | 3 +- worlds/stardew_valley/data/skill.py | 14 ++++ worlds/stardew_valley/early_items.py | 11 ++- worlds/stardew_valley/items.py | 35 ++++---- worlds/stardew_valley/locations.py | 81 ++++++++++--------- worlds/stardew_valley/logic/crafting_logic.py | 5 +- worlds/stardew_valley/logic/grind_logic.py | 3 +- worlds/stardew_valley/logic/mine_logic.py | 25 ++++-- worlds/stardew_valley/logic/skill_logic.py | 27 ++++--- .../mods/logic/deepwoods_logic.py | 7 +- .../stardew_valley/mods/logic/skills_logic.py | 11 +-- worlds/stardew_valley/regions.py | 22 ++--- worlds/stardew_valley/rules.py | 67 ++++----------- worlds/stardew_valley/test/TestRegions.py | 10 ++- .../stardew_valley/test/content/__init__.py | 3 +- worlds/stardew_valley/test/mods/TestMods.py | 5 +- 21 files changed, 244 insertions(+), 181 deletions(-) create mode 100644 worlds/stardew_valley/content/feature/skill_progression.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 135afa1c9726..34c617f5013a 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -148,7 +148,7 @@ def create_region(name: str, exits: Iterable[str]) -> Region: region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits] return region - world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options) + world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options, self.content) self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys()) self.modified_bundles = get_all_bundles(self.random, @@ -184,7 +184,7 @@ def create_items(self): self.multiworld.itempool += created_items - setup_early_items(self.multiworld, self.options, self.player, self.random) + setup_early_items(self.multiworld, self.options, self.content, self.player, self.random) self.setup_player_events() self.setup_victory() diff --git a/worlds/stardew_valley/bundles/bundle_item.py b/worlds/stardew_valley/bundles/bundle_item.py index 7dc9c0e1a3b5..91e279d2a623 100644 --- a/worlds/stardew_valley/bundles/bundle_item.py +++ b/worlds/stardew_valley/bundles/bundle_item.py @@ -3,8 +3,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from ..content import StardewContent -from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations, SkillProgression +from ..content import StardewContent, content_packs +from ..options import StardewValleyOptions, FestivalLocations from ..strings.crop_names import Fruit from ..strings.currency_names import Currency from ..strings.quality_names import CropQuality, FishQuality, ForageQuality @@ -12,34 +12,35 @@ class BundleItemSource(ABC): @abstractmethod - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: ... class VanillaItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: return True class IslandItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: - return options.exclude_ginger_island == ExcludeGingerIsland.option_false + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + return content_packs.ginger_island_content_pack.name in content.registered_packs class FestivalItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: return options.festival_locations != FestivalLocations.option_disabled +# FIXME remove this once recipes are in content packs class MasteryItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: - return options.skill_progression == SkillProgression.option_progressive_with_masteries + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + return content.features.skill_progression.are_masteries_shuffled class ContentItemSource(BundleItemSource): """This is meant to be used for items that are managed by the content packs.""" - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: raise ValueError("This should not be called, check if the item is in the content instead.") @@ -97,5 +98,4 @@ def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> if isinstance(self.source, ContentItemSource): return self.get_item() in content.game_items - return self.source.can_appear(options) - + return self.source.can_appear(content, options) diff --git a/worlds/stardew_valley/content/__init__.py b/worlds/stardew_valley/content/__init__.py index 9130873fa405..54b4d75d5e5c 100644 --- a/worlds/stardew_valley/content/__init__.py +++ b/worlds/stardew_valley/content/__init__.py @@ -1,5 +1,5 @@ from . import content_packs -from .feature import cropsanity, friendsanity, fishsanity, booksanity +from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression from .game_content import ContentPack, StardewContent, StardewFeatures from .unpacking import unpack_content from .. import options @@ -31,7 +31,8 @@ def choose_features(player_options: options.StardewValleyOptions) -> StardewFeat choose_booksanity(player_options.booksanity), choose_cropsanity(player_options.cropsanity), choose_fishsanity(player_options.fishsanity), - choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size) + choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size), + choose_skill_progression(player_options.skill_progression), ) @@ -105,3 +106,19 @@ def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: o return friendsanity.FriendsanityAllWithMarriage(heart_size.value) raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}") + + +skill_progression_by_option = { + options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(), + options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(), + options.SkillProgression.option_progressive_with_masteries: skill_progression.SkillProgressionProgressiveWithMasteries(), +} + + +def choose_skill_progression(skill_progression_option: options.SkillProgression) -> skill_progression.SkillProgressionFeature: + skill_progression_feature = skill_progression_by_option.get(skill_progression_option) + + if skill_progression_feature is None: + raise ValueError(f"No skill progression feature mapped to {str(skill_progression_option.value)}") + + return skill_progression_feature diff --git a/worlds/stardew_valley/content/feature/__init__.py b/worlds/stardew_valley/content/feature/__init__.py index 74249c808257..f3e5c6732e32 100644 --- a/worlds/stardew_valley/content/feature/__init__.py +++ b/worlds/stardew_valley/content/feature/__init__.py @@ -2,3 +2,4 @@ from . import cropsanity from . import fishsanity from . import friendsanity +from . import skill_progression diff --git a/worlds/stardew_valley/content/feature/skill_progression.py b/worlds/stardew_valley/content/feature/skill_progression.py new file mode 100644 index 000000000000..1325d4b35ff2 --- /dev/null +++ b/worlds/stardew_valley/content/feature/skill_progression.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Iterable, Tuple + +from ...data.skill import Skill + + +class SkillProgressionFeature(ABC): + is_progressive: ClassVar[bool] + are_masteries_shuffled: ClassVar[bool] + + @abstractmethod + def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]: + ... + + @abstractmethod + def is_mastery_randomized(self, skill: Skill) -> bool: + ... + + +class SkillProgressionVanilla(SkillProgressionFeature): + is_progressive = False + are_masteries_shuffled = False + + def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]: + return () + + def is_mastery_randomized(self, skill: Skill) -> bool: + return False + + +class SkillProgressionProgressive(SkillProgressionFeature): + is_progressive = True + are_masteries_shuffled = False + + def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]: + return skill.level_names_by_level + + def is_mastery_randomized(self, skill: Skill) -> bool: + return False + + +class SkillProgressionProgressiveWithMasteries(SkillProgressionProgressive): + are_masteries_shuffled = True + + def is_mastery_randomized(self, skill: Skill) -> bool: + return skill.has_mastery diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py index 8dcf933145e3..7ff3217b04ed 100644 --- a/worlds/stardew_valley/content/game_content.py +++ b/worlds/stardew_valley/content/game_content.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union -from .feature import booksanity, cropsanity, fishsanity, friendsanity +from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression from ..data.fish_data import FishItem from ..data.game_item import GameItem, ItemSource, ItemTag from ..data.skill import Skill @@ -53,6 +53,7 @@ class StardewFeatures: cropsanity: cropsanity.CropsanityFeature fishsanity: fishsanity.FishsanityFeature friendsanity: friendsanity.FriendsanityFeature + skill_progression: skill_progression.SkillProgressionFeature @dataclass(frozen=True) diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py index 4c754ddd8716..df4ff9feed6d 100644 --- a/worlds/stardew_valley/data/skill.py +++ b/worlds/stardew_valley/data/skill.py @@ -1,7 +1,21 @@ from dataclasses import dataclass, field +from functools import cached_property +from typing import Iterable, Tuple @dataclass(frozen=True) class Skill: name: str has_mastery: bool = field(kw_only=True) + + @cached_property + def mastery_name(self) -> str: + return f"{self.name} Mastery" + + @cached_property + def level_name(self) -> str: + return f"{self.name} Level" + + @cached_property + def level_names_by_level(self) -> Iterable[Tuple[int, str]]: + return tuple((level, f"Level {level} {self.name}") for level in range(1, 11)) diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py index e1ad8cebfd4a..81e28956b3cf 100644 --- a/worlds/stardew_valley/early_items.py +++ b/worlds/stardew_valley/early_items.py @@ -1,11 +1,13 @@ from random import Random from . import options as stardew_options +from .content import StardewContent from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.transport_names import Transportation from .strings.building_names import Building from .strings.region_names import Region from .strings.season_names import Season +from .strings.skill_names import Skill from .strings.tv_channel_names import Channel from .strings.wallet_item_names import Wallet @@ -14,7 +16,7 @@ seasons = [Season.spring, Season.summer, Season.fall, Season.winter] -def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, player: int, random: Random): +def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, content: StardewContent, player: int, random: Random): early_forced = [] early_candidates = [] early_candidates.extend(always_early_candidates) @@ -31,12 +33,13 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, early_forced.append("Progressive Backpack") if options.tool_progression & stardew_options.ToolProgression.option_progressive: - if options.fishsanity != stardew_options.Fishsanity.option_none: + if content.features.fishsanity.is_enabled: early_candidates.append("Progressive Fishing Rod") early_forced.append("Progressive Pickaxe") - if options.skill_progression == stardew_options.SkillProgression.option_progressive: - early_forced.append("Fishing Level") + fishing = content.skills.get(Skill.fishing) + if fishing is not None and content.features.skill_progression.is_progressive: + early_forced.append(fishing.level_name) if options.quest_locations >= 0: early_candidates.append(Wallet.magnifying_glass) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 5bf59a5e8397..3d852a37f402 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -15,7 +15,7 @@ from .logic.logic_event import all_events from .mods.mod_data import ModNames from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ - BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ + BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs from .strings.ap_names.ap_option_names import OptionName from .strings.ap_names.ap_weapon_names import APWeapon @@ -226,8 +226,8 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley create_weapons(item_factory, options, items) items.append(item_factory("Skull Key")) create_elevators(item_factory, options, items) - create_tools(item_factory, options, items) - create_skills(item_factory, options, items) + create_tools(item_factory, options, content, items) + create_skills(item_factory, content, items) create_wizard_buildings(item_factory, options, items) create_carpenter_buildings(item_factory, options, items) items.append(item_factory("Railroad Boulder Removed")) @@ -316,7 +316,7 @@ def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOpt items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8]) -def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): if options.tool_progression & ToolProgression.option_progressive: for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]: name = item_data.name @@ -325,28 +325,29 @@ def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions items.append(item_factory(item_data, ItemClassification.useful)) else: items.extend([item_factory(item) for item in [item_data] * 4]) - if options.skill_progression == SkillProgression.option_progressive_with_masteries: + + if content.features.skill_progression.are_masteries_shuffled: + # Masteries add another tier to the scythe and the fishing rod items.append(item_factory("Progressive Scythe")) items.append(item_factory("Progressive Fishing Rod")) + + # The golden scythe is always randomized items.append(item_factory("Progressive Scythe")) -def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.skill_progression == SkillProgression.option_vanilla: +def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + skill_progression = content.features.skill_progression + if not skill_progression.is_progressive: return - for item in items_by_group[Group.SKILL_LEVEL_UP]: - if item.mod_name not in options.mods and item.mod_name is not None: - continue - items.extend(item_factory(item) for item in [item.name] * 10) + for skill in content.skills.values(): + items.extend(item_factory(skill.level_name) for _ in skill_progression.get_randomized_level_names_by_level(skill)) - if options.skill_progression != SkillProgression.option_progressive_with_masteries: - return + if skill_progression.is_mastery_randomized(skill): + items.append(item_factory(skill.mastery_name)) - for item in items_by_group[Group.SKILL_MASTERY]: - if item.mod_name not in options.mods and item.mod_name is not None: - continue - items.append(item_factory(item)) + if skill_progression.are_masteries_shuffled: + items.append(item_factory(Wallet.mastery_of_the_five_ways)) def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 1d67d535ccee..b3a8db6f0341 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -11,7 +11,7 @@ from .data.museum_data import all_museum_items from .mods.mod_data import ModNames from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \ - FestivalLocations, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType + FestivalLocations, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity from .strings.goal_names import Goal from .strings.quest_names import ModQuest, Quest @@ -188,12 +188,12 @@ def extend_cropsanity_locations(randomized_locations: List[LocationData], conten for item in content.find_tagged_items(ItemTag.CROPSANITY)) -def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): if options.quest_locations < 0: return story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST] - story_quest_locations = filter_disabled_locations(options, story_quest_locations) + story_quest_locations = filter_disabled_locations(options, content, story_quest_locations) randomized_locations.extend(story_quest_locations) for i in range(0, options.quest_locations.value): @@ -284,9 +284,9 @@ def extend_desert_festival_chef_locations(randomized_locations: List[LocationDat randomized_locations.extend(locations_to_add) -def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): if options.special_order_locations & SpecialOrderLocations.option_board: - board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) + board_locations = filter_disabled_locations(options, content, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) randomized_locations.extend(board_locations) include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false @@ -308,9 +308,9 @@ def extend_walnut_purchase_locations(randomized_locations: List[LocationData], o randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE]) -def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]] - filtered_mandatory_locations = filter_disabled_locations(options, mandatory_locations) + filtered_mandatory_locations = filter_disabled_locations(options, content, mandatory_locations) randomized_locations.extend(filtered_mandatory_locations) @@ -349,32 +349,32 @@ def extend_elevator_locations(randomized_locations: List[LocationData], options: randomized_locations.extend(filtered_elevator_locations) -def extend_monstersanity_locations(randomized_locations: List[LocationData], options): +def extend_monstersanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): monstersanity = options.monstersanity if monstersanity == Monstersanity.option_none: return if monstersanity == Monstersanity.option_one_per_monster or monstersanity == Monstersanity.option_split_goals: monster_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_MONSTER]] - filtered_monster_locations = filter_disabled_locations(options, monster_locations) + filtered_monster_locations = filter_disabled_locations(options, content, monster_locations) randomized_locations.extend(filtered_monster_locations) return goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_GOALS]] - filtered_goal_locations = filter_disabled_locations(options, goal_locations) + filtered_goal_locations = filter_disabled_locations(options, content, goal_locations) randomized_locations.extend(filtered_goal_locations) if monstersanity != Monstersanity.option_progressive_goals: return progressive_goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_PROGRESSIVE_GOALS]] - filtered_progressive_goal_locations = filter_disabled_locations(options, progressive_goal_locations) + filtered_progressive_goal_locations = filter_disabled_locations(options, content, progressive_goal_locations) randomized_locations.extend(filtered_progressive_goal_locations) -def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): shipsanity = options.shipsanity if shipsanity == Shipsanity.option_none: return if shipsanity == Shipsanity.option_everything: ship_locations = [location for location in locations_by_tag[LocationTags.SHIPSANITY]] - filtered_ship_locations = filter_disabled_locations(options, ship_locations) + filtered_ship_locations = filter_disabled_locations(options, content, ship_locations) randomized_locations.extend(filtered_ship_locations) return shipsanity_locations = set() @@ -385,11 +385,11 @@ def extend_shipsanity_locations(randomized_locations: List[LocationData], option if shipsanity == Shipsanity.option_full_shipment or shipsanity == Shipsanity.option_full_shipment_with_fish: shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]}) - filtered_shipsanity_locations = filter_disabled_locations(options, list(shipsanity_locations)) + filtered_shipsanity_locations = filter_disabled_locations(options, content, list(shipsanity_locations)) randomized_locations.extend(filtered_shipsanity_locations) -def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): cooksanity = options.cooksanity if cooksanity == Cooksanity.option_none: return @@ -398,11 +398,11 @@ def extend_cooksanity_locations(randomized_locations: List[LocationData], option else: cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY]) - filtered_cooksanity_locations = filter_disabled_locations(options, cooksanity_locations) + filtered_cooksanity_locations = filter_disabled_locations(options, content, cooksanity_locations) randomized_locations.extend(filtered_cooksanity_locations) -def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): chefsanity = options.chefsanity if chefsanity == Chefsanity.option_none: return @@ -418,16 +418,16 @@ def extend_chefsanity_locations(randomized_locations: List[LocationData], option if chefsanity & Chefsanity.option_skills: chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_SKILL]}) - filtered_chefsanity_locations = filter_disabled_locations(options, list(chefsanity_locations_by_name.values())) + filtered_chefsanity_locations = filter_disabled_locations(options, content, list(chefsanity_locations_by_name.values())) randomized_locations.extend(filtered_chefsanity_locations) -def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): if options.craftsanity == Craftsanity.option_none: return craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]] - filtered_craftsanity_locations = filter_disabled_locations(options, craftsanity_locations) + filtered_craftsanity_locations = filter_disabled_locations(options, content, craftsanity_locations) randomized_locations.extend(filtered_craftsanity_locations) @@ -467,7 +467,7 @@ def create_locations(location_collector: StardewLocationCollector, random: Random): randomized_locations = [] - extend_mandatory_locations(randomized_locations, options) + extend_mandatory_locations(randomized_locations, options, content) extend_bundle_locations(randomized_locations, bundle_rooms) extend_backpack_locations(randomized_locations, options) @@ -476,13 +476,12 @@ def create_locations(location_collector: StardewLocationCollector, extend_elevator_locations(randomized_locations, options) - if not options.skill_progression == SkillProgression.option_vanilla: - for location in locations_by_tag[LocationTags.SKILL_LEVEL]: - if location.mod_name is not None and location.mod_name not in options.mods: - continue - if LocationTags.MASTERY_LEVEL in location.tags and options.skill_progression != SkillProgression.option_progressive_with_masteries: - continue - randomized_locations.append(location_table[location.name]) + skill_progression = content.features.skill_progression + if skill_progression.is_progressive: + for skill in content.skills.values(): + randomized_locations.extend([location_table[location_name] for _, location_name in skill_progression.get_randomized_level_names_by_level(skill)]) + if skill_progression.is_mastery_randomized(skill): + randomized_locations.append(location_table[skill.mastery_name]) if options.building_progression & BuildingProgression.option_progressive: for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: @@ -501,15 +500,15 @@ def create_locations(location_collector: StardewLocationCollector, extend_friendsanity_locations(randomized_locations, content) extend_festival_locations(randomized_locations, options, random) - extend_special_order_locations(randomized_locations, options) + extend_special_order_locations(randomized_locations, options, content) extend_walnut_purchase_locations(randomized_locations, options) - extend_monstersanity_locations(randomized_locations, options) - extend_shipsanity_locations(randomized_locations, options) - extend_cooksanity_locations(randomized_locations, options) - extend_chefsanity_locations(randomized_locations, options) - extend_craftsanity_locations(randomized_locations, options) - extend_quests_locations(randomized_locations, options) + extend_monstersanity_locations(randomized_locations, options, content) + extend_shipsanity_locations(randomized_locations, options, content) + extend_cooksanity_locations(randomized_locations, options, content) + extend_chefsanity_locations(randomized_locations, options, content) + extend_craftsanity_locations(randomized_locations, options, content) + extend_quests_locations(randomized_locations, options, content) extend_book_locations(randomized_locations, content) extend_walnutsanity_locations(randomized_locations, options) @@ -538,19 +537,21 @@ def filter_qi_order_locations(options: StardewValleyOptions, locations: Iterable return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags) -def filter_masteries_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - include_masteries = options.skill_progression == SkillProgression.option_progressive_with_masteries - return (location for location in locations if include_masteries or LocationTags.REQUIRES_MASTERIES not in location.tags) +def filter_masteries_locations(content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]: + # FIXME Remove once recipes are handled by the content packs + if content.features.skill_progression.are_masteries_shuffled: + return locations + return (location for location in locations if LocationTags.REQUIRES_MASTERIES not in location.tags) def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: return (location for location in locations if location.mod_name is None or location.mod_name in options.mods) -def filter_disabled_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: +def filter_disabled_locations(options: StardewValleyOptions, content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]: locations_farm_filter = filter_farm_type(options, locations) locations_island_filter = filter_ginger_island(options, locations_farm_filter) locations_qi_filter = filter_qi_order_locations(options, locations_island_filter) - locations_masteries_filter = filter_masteries_locations(options, locations_qi_filter) + locations_masteries_filter = filter_masteries_locations(content, locations_qi_filter) locations_mod_filter = filter_modded_locations(options, locations_masteries_filter) return locations_mod_filter diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index 0403230eee34..28bf0d2af22c 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -16,7 +16,7 @@ from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource from ..locations import locations_by_tag, LocationTags -from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression +from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland from ..stardew_rule import StardewRule, True_, False_ from ..strings.region_names import Region @@ -101,12 +101,13 @@ def can_craft_everything(self) -> StardewRule: craftsanity_prefix = "Craft " all_recipes_names = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_masteries = self.options.skill_progression != SkillProgression.option_progressive_with_masteries + exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled for location in locations_by_tag[LocationTags.CRAFTSANITY]: if not location.name.startswith(craftsanity_prefix): continue if exclude_island and LocationTags.GINGER_ISLAND in location.tags: continue + # FIXME Remove when recipes are in content packs if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags: continue if location.mod_name and location.mod_name not in self.options.mods: diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py index e0ac84639d9c..997300ae7a54 100644 --- a/worlds/stardew_valley/logic/grind_logic.py +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -7,7 +7,6 @@ from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin -from ..options import Booksanity from ..stardew_rule import StardewRule, HasProgressionPercent from ..strings.book_names import Book from ..strings.craftable_names import Consumable @@ -39,7 +38,7 @@ def can_grind_mystery_boxes(self, quantity: int) -> StardewRule: opening_rule = self.logic.region.can_reach(Region.blacksmith) mystery_box_rule = self.logic.has(Consumable.mystery_box) book_of_mysteries_rule = self.logic.true_ \ - if self.options.booksanity == Booksanity.option_none \ + if not self.content.features.booksanity.is_enabled \ else self.logic.book.has_book_power(Book.book_of_mysteries) # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. time_rule = self.logic.time.has_lived_months(quantity // 14) diff --git a/worlds/stardew_valley/logic/mine_logic.py b/worlds/stardew_valley/logic/mine_logic.py index 61eba41ffe07..350582ae0dbb 100644 --- a/worlds/stardew_valley/logic/mine_logic.py +++ b/worlds/stardew_valley/logic/mine_logic.py @@ -58,14 +58,19 @@ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: rules = [] weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier) rules.append(weapon_rule) + if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) - if self.options.skill_progression >= options.SkillProgression.option_progressive: - skill_tier = min(10, max(0, tier * 2)) - rules.append(self.logic.skill.has_level(Skill.combat, skill_tier)) - rules.append(self.logic.skill.has_level(Skill.mining, skill_tier)) + + # No alternative for vanilla because we assume that you will grind the levels in the mines. + if self.content.features.skill_progression.is_progressive: + skill_level = min(10, max(0, tier * 2)) + rules.append(self.logic.skill.has_level(Skill.combat, skill_level)) + rules.append(self.logic.skill.has_level(Skill.mining, skill_level)) + if tier >= 4: rules.append(self.logic.cooking.can_cook()) + return self.logic.and_(*rules) @cache_self1 @@ -82,10 +87,14 @@ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule rules = [] weapon_rule = self.logic.combat.has_great_weapon rules.append(weapon_rule) + if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) - if self.options.skill_progression >= options.SkillProgression.option_progressive: - skill_tier = min(10, max(0, tier * 2 + 6)) - rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier), - self.logic.skill.has_level(Skill.mining, skill_tier)}) + + # No alternative for vanilla because we assume that you will grind the levels in the mines. + if self.content.features.skill_progression.is_progressive: + skill_level = min(10, max(0, tier * 2 + 6)) + rules.extend((self.logic.skill.has_level(Skill.combat, skill_level), + self.logic.skill.has_level(Skill.mining, skill_level))) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 17fabca28d95..bc2f6cb1263d 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -11,7 +11,6 @@ from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from .. import options from ..data.harvest import HarvestCropSource from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_skills_levels import get_mod_skill_levels @@ -77,21 +76,21 @@ def has_level(self, skill: str, level: int) -> StardewRule: if level == 0: return true_ - if self.options.skill_progression == options.SkillProgression.option_vanilla: - return self.logic.skill.can_earn_level(skill, level) + if self.content.features.skill_progression.is_progressive: + return self.logic.received(f"{skill} Level", level) - return self.logic.received(f"{skill} Level", level) + return self.logic.skill.can_earn_level(skill, level) def has_previous_level(self, skill: str, level: int) -> StardewRule: assert level > 0, f"There is no level before level 0." if level == 1: return true_ - if self.options.skill_progression == options.SkillProgression.option_vanilla: - months = max(1, level - 1) - return self.logic.time.has_lived_months(months) + if self.content.features.skill_progression.is_progressive: + return self.logic.received(f"{skill} Level", level - 1) - return self.logic.received(f"{skill} Level", level - 1) + months = max(1, level - 1) + return self.logic.time.has_lived_months(months) @cache_self1 def has_farming_level(self, level: int) -> StardewRule: @@ -102,7 +101,7 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star if level <= 0: return True_() - if self.options.skill_progression >= options.SkillProgression.option_progressive: + if self.content.features.skill_progression.is_progressive: skills_items = vanilla_skill_items if allow_modded_skills: skills_items += get_mod_skill_levels(self.options.mods) @@ -148,7 +147,7 @@ def can_get_combat_xp(self) -> StardewRule: @cached_property def can_get_fishing_xp(self) -> StardewRule: - if self.options.skill_progression >= options.SkillProgression.option_progressive: + if self.content.features.skill_progression.is_progressive: return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot return self.logic.skill.can_fish() @@ -178,7 +177,9 @@ def can_crab_pot_at(self, region: str) -> StardewRule: @cached_property def can_crab_pot(self) -> StardewRule: crab_pot_rule = self.logic.has(Fishing.bait) - if self.options.skill_progression >= options.SkillProgression.option_progressive: + + # We can't use the same rule if skills are vanilla, because fishing levels are required to crab pot, which is required to get fishing levels... + if self.content.features.skill_progression.is_progressive: crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot) else: crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp @@ -200,14 +201,14 @@ def can_earn_mastery(self, skill: str) -> StardewRule: return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave) def has_mastery(self, skill: str) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + if self.content.features.skill_progression.are_masteries_shuffled: return self.logic.received(f"{skill} Mastery") return self.logic.skill.can_earn_mastery(skill) @cached_property def can_enter_mastery_cave(self) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + if self.content.features.skill_progression.are_masteries_shuffled: return self.logic.received(Wallet.mastery_of_the_five_ways) return self.has_any_skills_maxed(included_modded_skills=False) diff --git a/worlds/stardew_valley/mods/logic/deepwoods_logic.py b/worlds/stardew_valley/mods/logic/deepwoods_logic.py index 26704eb7d11b..6e0eadfd5486 100644 --- a/worlds/stardew_valley/mods/logic/deepwoods_logic.py +++ b/worlds/stardew_valley/mods/logic/deepwoods_logic.py @@ -1,6 +1,5 @@ from typing import Union -from ... import options from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin @@ -45,9 +44,9 @@ def can_reach_woods_depth(self, depth: int) -> StardewRule: self.logic.received(ModTransportation.woods_obelisk)) tier = int(depth / 25) + 1 - if self.options.skill_progression >= options.SkillProgression.option_progressive: - combat_tier = min(10, max(0, tier + 5)) - rules.append(self.logic.skill.has_level(Skill.combat, combat_tier)) + if self.content.features.skill_progression.is_progressive: + combat_level = min(10, max(0, tier + 5)) + rules.append(self.logic.skill.has_level(Skill.combat, combat_level)) return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/mods/logic/skills_logic.py b/worlds/stardew_valley/mods/logic/skills_logic.py index cb12274dc651..ba9d27741807 100644 --- a/worlds/stardew_valley/mods/logic/skills_logic.py +++ b/worlds/stardew_valley/mods/logic/skills_logic.py @@ -13,7 +13,6 @@ from ...logic.relationship_logic import RelationshipLogicMixin from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames -from ...options import SkillProgression from ...stardew_rule import StardewRule, False_, True_, And from ...strings.building_names import Building from ...strings.craftable_names import ModCraftable, ModMachine @@ -37,7 +36,7 @@ def has_mod_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - if self.options.skill_progression == SkillProgression.option_progressive: + if self.content.features.skill_progression.is_progressive: return self.logic.received(f"{skill} Level", level) return self.can_earn_mod_skill_level(skill, level) @@ -85,13 +84,15 @@ def can_earn_socializing_skill_level(self, level: int) -> StardewRule: def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: shifter_rule = True_() preservation_rule = True_() - if self.options.skill_progression == self.options.skill_progression.option_progressive: + if self.content.features.skill_progression.is_progressive: shifter_rule = self.logic.has(ModCraftable.water_shifter) preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) if level >= 8: - return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule + tool_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold) + return tool_rule & shifter_rule & preservation_rule if level >= 5: - return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule + tool_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron) + return tool_rule & shifter_rule if level >= 3: return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 5b7db5ac79d1..d59439a4879d 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,8 +2,9 @@ from typing import Iterable, Dict, Protocol, List, Tuple, Set from BaseClasses import Region, Entrance +from .content import content_packs, StardewContent from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod -from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions, SkillProgression +from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag from .strings.entrance_names import Entrance, LogicEntrance from .strings.region_names import Region, LogicRegion @@ -587,7 +588,7 @@ def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionD return updated_region -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) \ +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions, content: StardewContent) \ -> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: entrances_data, regions_data = create_final_connections_and_regions(world_options) regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data} @@ -598,7 +599,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options: if entrance.name in entrances_data } - connections, randomized_data = randomize_connections(random, world_options, regions_data, entrances_data) + connections, randomized_data = randomize_connections(random, world_options, content, regions_data, entrances_data) for connection in connections: if connection.name in entrances_by_name: @@ -606,7 +607,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options: return regions_by_name, entrances_by_name, randomized_data -def randomize_connections(random: Random, world_options: StardewValleyOptions, regions_by_name: Dict[str, RegionData], +def randomize_connections(random: Random, world_options: StardewValleyOptions, content: StardewContent, regions_by_name: Dict[str, RegionData], connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]: connections_to_randomize: List[ConnectionData] = [] if world_options.entrance_randomization == EntranceRandomization.option_pelican_town: @@ -621,7 +622,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r elif world_options.entrance_randomization == EntranceRandomization.option_chaos: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.BUILDINGS in connections_by_name[connection].flag] - connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options) + connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content) # On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day randomized_data_for_mod = {} @@ -630,7 +631,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r randomized_data_for_mod[connection.reverse] = connection.reverse return list(connections_by_name.values()), randomized_data_for_mod - connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options) + connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content) random.shuffle(connections_to_randomize) destination_pool = list(connections_to_randomize) random.shuffle(destination_pool) @@ -645,12 +646,11 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r return randomized_connections_for_generation, randomized_data_for_mod -def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], world_options: StardewValleyOptions) -> List[ConnectionData]: - exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true - if exclude_island: +def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], content: StardewContent) -> List[ConnectionData]: + # FIXME remove when regions are handled in content packs + if content_packs.ginger_island_content_pack.name not in content.registered_packs: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] - exclude_masteries = world_options.skill_progression != SkillProgression.option_progressive_with_masteries - if exclude_masteries: + if not content.features.skill_progression.are_masteries_shuffled: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag] return connections_to_randomize diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index e7107e89f948..96f081788041 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -21,7 +21,7 @@ from .mods.mod_data import ModNames from .options import StardewValleyOptions, Walnutsanity from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ - Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, SkillProgression + Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity from .stardew_rule import And, StardewRule, true_ from .stardew_rule.indirect_connection import look_for_indirect_connection from .stardew_rule.rule_explain import explain @@ -47,7 +47,7 @@ from .strings.quest_names import Quest from .strings.region_names import Region from .strings.season_names import Season -from .strings.skill_names import ModSkill, Skill +from .strings.skill_names import Skill from .strings.tool_names import Tool, ToolMaterial from .strings.tv_channel_names import Channel from .strings.villager_names import NPC, ModNPC @@ -70,7 +70,7 @@ def set_rules(world): set_ginger_island_rules(logic, multiworld, player, world_options) set_tool_rules(logic, multiworld, player, world_options) - set_skills_rules(logic, multiworld, player, world_options) + set_skills_rules(logic, multiworld, player, world_content) set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options) set_building_rules(logic, multiworld, player, world_options) set_cropsanity_rules(logic, multiworld, player, world_content) @@ -164,58 +164,21 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules)) -def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - mods = world_options.mods - if world_options.skill_progression == SkillProgression.option_vanilla: +def set_skills_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent): + skill_progression = content.features.skill_progression + if not skill_progression.is_progressive: return - for i in range(1, 11): - set_vanilla_skill_rule_for_level(logic, multiworld, player, i) - set_modded_skill_rule_for_level(logic, multiworld, player, mods, i) + for skill in content.skills.values(): + for level, level_name in skill_progression.get_randomized_level_names_by_level(skill): + rule = logic.skill.can_earn_level(skill.name, level) + location = multiworld.get_location(level_name, player) + MultiWorldRules.set_rule(location, rule) - if world_options.skill_progression == SkillProgression.option_progressive: - return - - for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]: - MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill)) - - -def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): - set_vanilla_skill_rule(logic, multiworld, player, Skill.farming, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.fishing, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.foraging, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.mining, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.combat, level) - - -def set_modded_skill_rule_for_level(logic: StardewLogic, multiworld, player, mods, level: int): - if ModNames.luck_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.luck, level) - if ModNames.magic in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.magic, level) - if ModNames.binning_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.binning, level) - if ModNames.cooking_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.cooking, level) - if ModNames.socializing_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.socializing, level) - if ModNames.archaeology in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.archaeology, level) - - -def get_skill_level_location(multiworld, player, skill: str, level: int): - location_name = f"Level {level} {skill}" - return multiworld.get_location(location_name, player) - - -def set_vanilla_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int): - rule = logic.skill.can_earn_level(skill, level) - MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule) - - -def set_modded_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int): - rule = logic.skill.can_earn_level(skill, level) - MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule) + if skill_progression.is_mastery_randomized(skill): + rule = logic.skill.can_earn_mastery(skill.name) + location = multiworld.get_location(skill.mastery_name, player) + MultiWorldRules.set_rule(location, rule) def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index a25feea22085..c2e962d88a7e 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -4,6 +4,7 @@ from BaseClasses import get_seed from . import SVTestCase, complete_options_with_default +from .. import create_content from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions from ..strings.entrance_names import Entrance as EntranceName @@ -63,11 +64,12 @@ def test_entrance_randomization(self): ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) + content = create_content(sv_options) seed = get_seed() rand = random.Random(seed) with self.subTest(flag=flag, msg=f"Seed: {seed}"): entrances, regions = create_final_connections_and_regions(sv_options) - _, randomized_connections = randomize_connections(rand, sv_options, regions, entrances) + _, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances) for connection in vanilla_connections: if flag in connection.flag: @@ -90,11 +92,12 @@ def test_entrance_randomization_without_island(self): ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) + content = create_content(sv_options) seed = get_seed() rand = random.Random(seed) with self.subTest(option=option, flag=flag, seed=seed): entrances, regions = create_final_connections_and_regions(sv_options) - _, randomized_connections = randomize_connections(rand, sv_options, regions, entrances) + _, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances) for connection in vanilla_connections: if flag in connection.flag: @@ -118,13 +121,14 @@ def test_cannot_put_island_access_on_island(self): ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) + content = create_content(sv_options) for i in range(0, 100 if self.skip_long_tests else 10000): seed = get_seed() rand = random.Random(seed) with self.subTest(msg=f"Seed: {seed}"): entrances, regions = create_final_connections_and_regions(sv_options) - randomized_connections, randomized_data = randomize_connections(rand, sv_options, regions, entrances) + randomized_connections, randomized_data = randomize_connections(rand, sv_options, content, regions, entrances) connections_by_name = {connection.name: connection for connection in randomized_connections} blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island} diff --git a/worlds/stardew_valley/test/content/__init__.py b/worlds/stardew_valley/test/content/__init__.py index 4130dae90dc3..c666a3aae14d 100644 --- a/worlds/stardew_valley/test/content/__init__.py +++ b/worlds/stardew_valley/test/content/__init__.py @@ -7,7 +7,8 @@ feature.booksanity.BooksanityDisabled(), feature.cropsanity.CropsanityDisabled(), feature.fishsanity.FishsanityNone(), - feature.friendsanity.FriendsanityNone() + feature.friendsanity.FriendsanityNone(), + feature.skill_progression.SkillProgressionVanilla(), ) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 07a75f21b1de..56138cf582a7 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -3,7 +3,7 @@ from BaseClasses import get_seed from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld from ..assertion import ModAssertMixin, WorldAssertMixin -from ... import items, Group, ItemClassification +from ... import items, Group, ItemClassification, create_content from ... import options from ...items import items_by_group from ...options import SkillProgression, Walnutsanity @@ -128,12 +128,13 @@ def test_mod_entrance_randomization(self): SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, options.Mods.internal_name: frozenset(options.Mods.valid_keys) }) + content = create_content(sv_options) seed = get_seed() rand = random.Random(seed) with self.subTest(option=option, flag=flag, seed=seed): final_connections, final_regions = create_final_connections_and_regions(sv_options) - _, randomized_connections = randomize_connections(rand, sv_options, final_regions, final_connections) + _, randomized_connections = randomize_connections(rand, sv_options, content, final_regions, final_connections) for connection_name in final_connections: connection = final_connections[connection_name] From 992657750c30094c8a4b5734104dcda8b0547622 Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:09:22 +0100 Subject: [PATCH 064/112] YGO06: add Item groups (#3737) * YGO06: adds item groups * YGO06: Change lists to sets Co-authored-by: Scipio Wright * YGO06: fix imports --------- Co-authored-by: Scipio Wright --- worlds/yugioh06/__init__.py | 26 ++++++++++++++++++-------- worlds/yugioh06/items.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 90bbed1a2174..9070683f33d5 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -1,6 +1,6 @@ import os import pkgutil -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Set import settings from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial @@ -17,12 +17,14 @@ draft_opponents, excluded_items, item_to_index, - tier_1_opponents, useful, + tier_1_opponents, + tier_2_opponents, + tier_3_opponents, + tier_4_opponents, + tier_5_opponents, ) -from .items import ( - challenges as challenges, -) +from .items import challenges as challenges from .locations import ( Bonuses, Campaign_Opponents, @@ -109,9 +111,17 @@ class Yugioh06World(World): for k, v in Required_Cards.items(): location_name_to_id[k] = v + start_id - item_name_groups = { - "Core Booster": core_booster, - "Campaign Boss Beaten": ["Tier 1 Beaten", "Tier 2 Beaten", "Tier 3 Beaten", "Tier 4 Beaten", "Tier 5 Beaten"], + item_name_groups: Dict[str, Set[str]] = { + "Core Booster": set(core_booster), + "Campaign Boss Beaten": {"Tier 1 Beaten", "Tier 2 Beaten", "Tier 3 Beaten", "Tier 4 Beaten", "Tier 5 Beaten"}, + "Challenge": set(challenges), + "Tier 1 Opponent": set(tier_1_opponents), + "Tier 2 Opponent": set(tier_2_opponents), + "Tier 3 Opponent": set(tier_3_opponents), + "Tier 4 Opponent": set(tier_4_opponents), + "Tier 5 Opponent": set(tier_5_opponents), + "Campaign Opponent": set(tier_1_opponents + tier_2_opponents + tier_3_opponents + + tier_4_opponents + tier_5_opponents) } removed_challenges: List[str] diff --git a/worlds/yugioh06/items.py b/worlds/yugioh06/items.py index f0f877fd9f7b..0cfcf32992f2 100644 --- a/worlds/yugioh06/items.py +++ b/worlds/yugioh06/items.py @@ -183,6 +183,35 @@ "Campaign Tier 1 Column 5", ] +tier_2_opponents: List[str] = [ + "Campaign Tier 2 Column 1", + "Campaign Tier 2 Column 2", + "Campaign Tier 2 Column 3", + "Campaign Tier 2 Column 4", + "Campaign Tier 2 Column 5", +] + +tier_3_opponents: List[str] = [ + "Campaign Tier 3 Column 1", + "Campaign Tier 3 Column 2", + "Campaign Tier 3 Column 3", + "Campaign Tier 3 Column 4", + "Campaign Tier 3 Column 5", +] + +tier_4_opponents: List[str] = [ + "Campaign Tier 4 Column 1", + "Campaign Tier 4 Column 2", + "Campaign Tier 4 Column 3", + "Campaign Tier 4 Column 4", + "Campaign Tier 4 Column 5", +] + +tier_5_opponents: List[str] = [ + "Campaign Tier 5 Column 1", + "Campaign Tier 5 Column 2", +] + Banlist_Items: List[str] = [ "No Banlist", "Banlist September 2003", From 6cfc3a46670232db873d7ce362c9e99e23ebaa03 Mon Sep 17 00:00:00 2001 From: Eric Newport Date: Sat, 30 Nov 2024 19:10:00 -0800 Subject: [PATCH 065/112] Docs: Improved sm64ex advanced setup docs (#3741) * Improved sm64ex advanced setup docs This edit clarifies some things that are not obvious in the version that is currently live on the site. This should prevent others from needing to go spelunking in Discord chat history to figure out how to do advanced builds. * Update worlds/sm64ex/docs/setup_en.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * copyediting --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/sm64ex/docs/setup_en.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index afb5bad50f71..9963d3945a10 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -29,15 +29,25 @@ Then continue to `Using the Launcher` *Using the Launcher* -1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry +1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry. 2. Scroll down, and download the zip file for your OS. -3. Unpack the zip file in an empty folder +3. Unpack the zip file in an empty folder. 4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps. - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. -5. When finished, use `Compile default SM64AP build` to continue - - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others. - - [Available Makeflags](https://github.com/sm64pc/sm64ex/wiki/Build-options) - - [Included Game Patches](https://github.com/N00byKing/sm64ex/blob/archipelago/enhancements/README.md) +5. When finished, use `Compile default SM64AP build` to continue. + - **Advanced configuration:** If you want to use additional build options such as Better Camera, No Drawing Distance, etc or apply game patches such as 60FPS, Enhanced Moveset, etc, then use the `Compile custom build` option: + - Set a name for your build, e.g. "archipelago" or whatever you like. + - Press the `Download Files` button. + - Set Make Flags, e.g. `-j8 BETTERCAMERA=1 NODRAWINGDISTANCE=1` to enable Better Camera and No Drawing Distance. + - Press `Apply Patches` to select patches to apply. Example patches include: + - 60FPS: Improves frame rate. + - Enhanced Moveset: Gives Mario new abilities. [Details here](https://github.com/TheGag96/sm64-port). + - Nonstop Mode: Makes it possible to fetch multiple stars in a level without exiting the level first. + - Press `Create Build`. This will take several minutes. + - You can also use the Repository and Branch fields to build with different repos or branches if you want to build using a fork or development version of SM64AP. + - For more details, see: + - [Available Makeflags](https://github.com/sm64pc/sm64ex/wiki/Build-options) + - [Included Game Patches](https://github.com/N00byKing/sm64ex/blob/archipelago/enhancements/README.md) 6. Press `Download Files` to prepare the build, afterwards `Create Build`. 7. SM64EX will now be compiled. This can take a while. From 3af2b1dc666cd03a2aa85044b24995adf0360833 Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Sat, 30 Nov 2024 19:10:43 -0800 Subject: [PATCH 066/112] id Tech 1 games: Add command line instructions/info (#3757) --- worlds/doom_1993/docs/setup_en.md | 20 +++++++++++++++++++- worlds/doom_ii/docs/setup_en.md | 20 +++++++++++++++++++- worlds/heretic/docs/setup_en.md | 19 ++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index 5d96e6a8056e..85061609abbb 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -17,7 +17,7 @@ You can find the folder in steam by finding the game in your library, right-clicking it and choosing **Manage -> Browse Local Files**. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Select `Ultimate DOOM` from the drop-down @@ -28,6 +28,24 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apdoom -game doom -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apfliplevels 0` will disable flipping levels. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (I'm too young to die) to 5 (Nightmare!) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md index ec6697c76da2..e444f85bd7c7 100644 --- a/worlds/doom_ii/docs/setup_en.md +++ b/worlds/doom_ii/docs/setup_en.md @@ -15,7 +15,7 @@ You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Select `DOOM II` from the drop-down @@ -26,6 +26,24 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apdoom -game doom2 -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apfliplevels 0` will disable flipping levels. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (I'm too young to die) to 5 (Nightmare!) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. diff --git a/worlds/heretic/docs/setup_en.md b/worlds/heretic/docs/setup_en.md index 41b7fdab8078..5985dbb0992a 100644 --- a/worlds/heretic/docs/setup_en.md +++ b/worlds/heretic/docs/setup_en.md @@ -15,7 +15,7 @@ You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Choose Heretic in the dropdown @@ -26,6 +26,23 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apheretic -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (thou needeth a wet-nurse) to 5 (black plague possesses thee) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. From 472d2d54061d3ebe88a4f2577f9e510445c74bcb Mon Sep 17 00:00:00 2001 From: Jarno Date: Sun, 1 Dec 2024 04:11:45 +0100 Subject: [PATCH 067/112] Timespinner: Implemented support for universal tracker (#3771) * Implemented slot data interpretation * Fixed talaria attached to be taken into logic --- worlds/timespinner/__init__.py | 83 ++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index a2a5c7ce9c78..ca31d08326b5 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Set, Tuple, TextIO +from typing import Dict, List, Set, Tuple, TextIO, Any, Optional from BaseClasses import Item, Tutorial, ItemClassification from .Items import get_item_names_per_category from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items @@ -55,13 +55,18 @@ def generate_early(self) -> None: self.precalculated_weights = PreCalculatedWeights(self.options, self.random) # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly - if self.options.start_inventory.value.pop('Meyef', 0) > 0: + if self.options.start_inventory.value.pop("Meyef", 0) > 0: self.options.start_with_meyef.value = Toggle.option_true - if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0: + if self.options.start_inventory.value.pop("Talaria Attachment", 0) > 0: self.options.quick_seed.value = Toggle.option_true - if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: + if self.options.start_inventory.value.pop("Jewelry Box", 0) > 0: self.options.start_with_jewelry_box.value = Toggle.option_true + self.interpret_slot_data(None) + + if self.options.quick_seed: + self.multiworld.push_precollected(self.create_item("Talaria Attachment")) + def create_regions(self) -> None: create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) @@ -144,6 +149,76 @@ def fill_slot_data(self) -> Dict[str, object]: "LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge, "Lab": self.precalculated_weights.flood_lab } + + def interpret_slot_data(self, slot_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """Used by Universal Tracker to correctly rebuild state""" + + if not slot_data \ + and hasattr(self.multiworld, "re_gen_passthrough") \ + and isinstance(self.multiworld.re_gen_passthrough, dict) \ + and "Timespinner" in self.multiworld.re_gen_passthrough: + slot_data = self.multiworld.re_gen_passthrough["Timespinner"] + + if not slot_data: + return None + + self.options.start_with_jewelry_box.value = slot_data["StartWithJewelryBox"] + self.options.downloadable_items.value = slot_data["DownloadableItems"] + self.options.eye_spy.value = slot_data["EyeSpy"] + self.options.start_with_meyef.value = slot_data["StartWithMeyef"] + self.options.quick_seed.value = slot_data["QuickSeed"] + self.options.specific_keycards.value = slot_data["SpecificKeycards"] + self.options.inverted.value = slot_data["Inverted"] + self.options.gyre_archives.value = slot_data["GyreArchives"] + self.options.cantoran.value = slot_data["Cantoran"] + self.options.lore_checks.value = slot_data["LoreChecks"] + self.options.boss_rando.value = slot_data["BossRando"] + self.options.damage_rando.value = slot_data["DamageRando"] + self.options.damage_rando_overrides.value = slot_data["DamageRandoOverrides"] + self.options.hp_cap.value = slot_data["HpCap"] + self.options.level_cap.value = slot_data["LevelCap"] + self.options.extra_earrings_xp.value = slot_data["ExtraEarringsXP"] + self.options.boss_healing.value = slot_data["BossHealing"] + self.options.shop_fill.value = slot_data["ShopFill"] + self.options.shop_warp_shards.value = slot_data["ShopWarpShards"] + self.options.shop_multiplier.value = slot_data["ShopMultiplier"] + self.options.loot_pool.value = slot_data["LootPool"] + self.options.drop_rate_category.value = slot_data["DropRateCategory"] + self.options.fixed_drop_rate.value = slot_data["FixedDropRate"] + self.options.loot_tier_distro.value = slot_data["LootTierDistro"] + self.options.show_bestiary.value = slot_data["ShowBestiary"] + self.options.show_drops.value = slot_data["ShowDrops"] + self.options.enter_sandman.value = slot_data["EnterSandman"] + self.options.dad_percent.value = slot_data["DadPercent"] + self.options.rising_tides.value = slot_data["RisingTides"] + self.options.unchained_keys.value = slot_data["UnchainedKeys"] + self.options.back_to_the_future.value = slot_data["PresentAccessWithWheelAndSpindle"] + self.options.traps.value = slot_data["Traps"] + self.options.death_link.value = slot_data["DeathLink"] + # Readonly slot_data["StinkyMaw"] + # data + # Readonly slot_data["PersonalItems"] + self.precalculated_weights.pyramid_keys_unlock = slot_data["PyramidKeysGate"] + self.precalculated_weights.present_key_unlock = slot_data["PresentGate"] + self.precalculated_weights.past_key_unlock = slot_data["PastGate"] + self.precalculated_weights.time_key_unlock = slot_data["TimeGate"] + # rising tides + if (slot_data["Basement"] > 1): + self.precalculated_weights.flood_basement = True + if (slot_data["Basement"] == 2): + self.precalculated_weights.flood_basement_high = True + self.precalculated_weights.flood_xarion = slot_data["Xarion"] + self.precalculated_weights.flood_maw = slot_data["Maw"] + self.precalculated_weights.flood_pyramid_shaft = slot_data["PyramidShaft"] + self.precalculated_weights.flood_pyramid_back = slot_data["BackPyramid"] + self.precalculated_weights.flood_moat = slot_data["CastleMoat"] + self.precalculated_weights.flood_courtyard = slot_data["CastleCourtyard"] + self.precalculated_weights.flood_lake_desolation = slot_data["LakeDesolation"] + self.precalculated_weights.flood_lake_serene = not slot_data["DryLakeSerene"] + self.precalculated_weights.flood_lake_serene_bridge = slot_data["LakeSereneBridge"] + self.precalculated_weights.flood_lab = slot_data["Lab"] + + return slot_data def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.unchained_keys: From 21dbfd2472a517f4885b018c24ef14173d6d6cae Mon Sep 17 00:00:00 2001 From: josephwhite Date: Sat, 30 Nov 2024 22:33:36 -0500 Subject: [PATCH 068/112] Multiserver: Add argument for timestamping STDOUT (#4266) * core: add server arg for timestamping STDOUT * Multiserver: Implicitly use default write_mode arg in init_logging Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- MultiServer.py | 6 +++++- Utils.py | 9 +++++---- settings.py | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 628817d5977c..80fcd32fd1e3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2378,6 +2378,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--cert_key', help="Path to SSL Certificate Key file") parser.add_argument('--loglevel', default=defaults["loglevel"], choices=['debug', 'info', 'warning', 'error', 'critical']) + parser.add_argument('--logtime', help="Add timestamps to STDOUT", + default=defaults["logtime"], action='store_true') parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int) parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int) parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true') @@ -2458,7 +2460,9 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte async def main(args: argparse.Namespace): - Utils.init_logging("Server", loglevel=args.loglevel.lower()) + Utils.init_logging(name="Server", + loglevel=args.loglevel.lower(), + add_timestamp=args.logtime) ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, diff --git a/Utils.py b/Utils.py index f5c6ca6414f5..4f99d26ac402 100644 --- a/Utils.py +++ b/Utils.py @@ -485,9 +485,9 @@ def get_text_after(text: str, start: str) -> str: loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} -def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w", - log_format: str = "[%(name)s at %(asctime)s]: %(message)s", - exception_logger: typing.Optional[str] = None): +def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, + write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s", + add_timestamp: bool = False, exception_logger: typing.Optional[str] = None): import datetime loglevel: int = loglevel_mapping.get(loglevel, loglevel) log_folder = user_path("logs") @@ -521,7 +521,8 @@ def filter(self, record: logging.LogRecord) -> bool: formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') stream_handler = logging.StreamHandler(sys.stdout) stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False))) - stream_handler.setFormatter(formatter) + if add_timestamp: + stream_handler.setFormatter(formatter) root_logger.addHandler(stream_handler) # Relay unhandled exceptions to logger. diff --git a/settings.py b/settings.py index ccd3458003c2..04d8760c3cd3 100644 --- a/settings.py +++ b/settings.py @@ -599,6 +599,7 @@ class LogNetwork(IntEnum): savefile: Optional[str] = None disable_save: bool = False loglevel: str = "info" + logtime: bool = False server_password: Optional[ServerPassword] = None disable_item_cheat: Union[DisableItemCheat, bool] = False location_check_points: LocationCheckPoints = LocationCheckPoints(1) From 1a5d22ca7890175275ef8b77c31cc0dce705d85a Mon Sep 17 00:00:00 2001 From: Benjamin S Wolf Date: Sat, 30 Nov 2024 19:51:26 -0800 Subject: [PATCH 069/112] Core: Add new error message for item count when defined as a set instead of a dict (#4100) * Core: New error message if item count is a set * Apply suggestion for error message Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply item count error suggestion Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Options.py b/Options.py index 0d0e6ae1cd95..d3b2e6c1ba11 100644 --- a/Options.py +++ b/Options.py @@ -863,6 +863,8 @@ class ItemDict(OptionDict): verify_item_name = True def __init__(self, value: typing.Dict[str, int]): + if any(item_count is None for item_count in value.values()): + raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .") if any(item_count < 1 for item_count in value.values()): raise Exception("Cannot have non-positive item counts.") super(ItemDict, self).__init__(value) From ed721dd0c14aec3cb332a010f52bca43fddc2886 Mon Sep 17 00:00:00 2001 From: Alex Nordstrom Date: Sat, 30 Nov 2024 22:58:10 -0500 Subject: [PATCH 070/112] LADX: Implement various upstream adjustments (#3829) * magnifying lens changes https://github.com/daid/LADXR/pull/156 * restore enemy visibility in mermaid statue cave https://github.com/daid/LADXR/pull/155 * mermaid statue scale bugfix https://github.com/daid/LADXR/pull/163 * restore vanilla map when rooster is an item https://github.com/daid/LADXR/pull/132 * fix * fixes to magnifying lens changes * load marin singing even if you have marin date https://github.com/daid/LADXR/commit/4feb3099a3f3a144739094f88a4024def6f79767 * Revert "load marin singing even if you have marin date" This reverts commit a7a546ed3f7a2b9c9bcb095984cc64319a4f7855. * always patch tradequest not upstream, but included in this PR because it touches the same parts of the code. https://discord.com/channels/731205301247803413/1227373762412937347 * marin date fix * fix logic --- worlds/ladx/LADXR/generator.py | 10 ++- worlds/ladx/LADXR/locations/birdKey.py | 17 ------ worlds/ladx/LADXR/locations/boomerangGuy.py | 5 -- worlds/ladx/LADXR/logic/overworld.py | 45 ++++++++------ worlds/ladx/LADXR/logic/requirements.py | 3 + worlds/ladx/LADXR/patches/maptweaks.py | 13 ++++ worlds/ladx/LADXR/patches/songs.py | 4 ++ worlds/ladx/LADXR/patches/tradeSequence.py | 68 ++++++++++++++++++--- worlds/ladx/Options.py | 2 +- 9 files changed, 110 insertions(+), 57 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 69e856f3541b..f0f042c67db8 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -153,7 +153,9 @@ def generateRom(args, world: "LinksAwakeningWorld"): if world.ladxr_settings.witch: patches.witch.updateWitch(rom) patches.softlock.fixAll(rom) - patches.maptweaks.tweakMap(rom) + if not world.ladxr_settings.rooster: + patches.maptweaks.tweakMap(rom) + patches.maptweaks.tweakBirdKeyRoom(rom) patches.chest.fixChests(rom) patches.shop.fixShop(rom) patches.rooster.patchRooster(rom) @@ -176,11 +178,7 @@ def generateRom(args, world: "LinksAwakeningWorld"): patches.songs.upgradeMarin(rom) patches.songs.upgradeManbo(rom) patches.songs.upgradeMamu(rom) - if world.ladxr_settings.tradequest: - patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang) - else: - # Monkey bridge patch, always have the bridge there. - rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) + patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings) patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal') if world.ladxr_settings.bowwow != 'normal': patches.bowwow.bowwowMapPatches(rom) diff --git a/worlds/ladx/LADXR/locations/birdKey.py b/worlds/ladx/LADXR/locations/birdKey.py index 12418c61aa46..0dbdd8653fe2 100644 --- a/worlds/ladx/LADXR/locations/birdKey.py +++ b/worlds/ladx/LADXR/locations/birdKey.py @@ -1,23 +1,6 @@ from .droppedKey import DroppedKey -from ..roomEditor import RoomEditor -from ..assembler import ASM class BirdKey(DroppedKey): def __init__(self): super().__init__(0x27A) - - def patch(self, rom, option, *, multiworld=None): - super().patch(rom, option, multiworld=multiworld) - - re = RoomEditor(rom, self.room) - - # Make the bird key accessible without the rooster - re.removeObject(1, 6) - re.removeObject(2, 6) - re.removeObject(3, 5) - re.removeObject(3, 6) - re.moveObject(1, 5, 2, 6) - re.moveObject(2, 5, 3, 6) - re.addEntity(3, 5, 0x9D) - re.store(rom) diff --git a/worlds/ladx/LADXR/locations/boomerangGuy.py b/worlds/ladx/LADXR/locations/boomerangGuy.py index 92d76cebdf5d..23fcc867617b 100644 --- a/worlds/ladx/LADXR/locations/boomerangGuy.py +++ b/worlds/ladx/LADXR/locations/boomerangGuy.py @@ -24,11 +24,6 @@ def configure(self, options): # But SHIELD, BOMB and MAGIC_POWDER would most likely break things. # SWORD and POWER_BRACELET would most likely introduce the lv0 shield/bracelet issue def patch(self, rom, option, *, multiworld=None): - # Always have the boomerang trade guy enabled (normally you need the magnifier) - rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy - rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout - rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True) - if self.setting == 'trade': inv = INVENTORY_MAP[option] # Patch the check if you traded back the boomerang (so traded twice) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index 551cf8353f4a..3972796051f9 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -61,9 +61,9 @@ def __init__(self, options, world_setup, r): self._addEntrance("banana_seller", sword_beach, banana_seller, r.bush) boomerang_cave = Location("Boomerang Cave") if options.boomerang == 'trade': - Location().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)) + Location().add(BoomerangGuy()).connect(boomerang_cave, AND(r.shuffled_magnifier, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))) elif options.boomerang == 'gift': - Location().add(BoomerangGuy()).connect(boomerang_cave, None) + Location().add(BoomerangGuy()).connect(boomerang_cave, r.shuffled_magnifier) self._addEntrance("boomerang_cave", sword_beach, boomerang_cave, BOMB) self._addEntranceRequirementExit("boomerang_cave", None) # if exiting, you do not need bombs @@ -167,7 +167,9 @@ def __init__(self, options, world_setup, r): prairie_island_seashell = Location().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3 Location().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house Location().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse - self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3 + self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), ROOSTER) + if not options.rooster: + self._addEntranceRequirement("castle_jump_cave", AND(FEATHER, PEGASUS_BOOTS)) # left of the castle, 5 holes turned into 3 Location().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock left_bay_area = Location() @@ -353,7 +355,7 @@ def __init__(self, options, world_setup, r): self._addEntrance("d4", d4_entrance, None, ANGLER_KEY) self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo - self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) + self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) # Raft game. raft_house = Location("Raft House") @@ -379,7 +381,9 @@ def __init__(self, options, world_setup, r): self._addEntrance("rooster_house", outside_rooster_house, None, None) bird_cave = Location() bird_key = Location().add(BirdKey()) - bird_cave.connect(bird_key, OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) + bird_cave.connect(bird_key, ROOSTER) + if not options.rooster: + bird_cave.connect(bird_key, AND(FEATHER, COUNT(POWER_BRACELET, 2))) # elephant statue added if options.logic != "casual": bird_cave.connect(lower_right_taltal, None, one_way=True) # Drop in a hole at bird cave self._addEntrance("bird_cave", outside_rooster_house, bird_cave, None) @@ -468,7 +472,7 @@ def __init__(self, options, world_setup, r): swamp.connect(writes_hut_outside, HOOKSHOT, one_way=True) # hookshot the sign in front of writes hut graveyard_heartpiece.connect(graveyard_cave_right, FEATHER) # jump to the bottom right tile around the blocks graveyard_heartpiece.connect(graveyard_cave_right, OR(HOOKSHOT, BOOMERANG)) # push bottom block, wall clip and hookshot/boomerang corner to grab item - + self._addEntranceRequirement("mamu", AND(FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), OR(MAGIC_POWDER, BOMB))) # use bombs or powder to get rid of a bush on the other side by jumping across and placing the bomb/powder before you fall into the pit fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # can talk to the fisherman from the water when the boat is low (requires swimming up out of the water a bit) @@ -476,9 +480,10 @@ def __init__(self, options, world_setup, r): castle_inside.connect(kanalet_chain_trooper, BOOMERANG, one_way=True) # kill the ball and chain trooper from the left side, then use boomerang to grab the dropped item animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(PEGASUS_BOOTS, FEATHER)) # jump across horizontal 4 gap to heart piece desert_lanmola.connect(desert, BOMB) # use bombs to kill lanmola - + d6_connector_left.connect(d6_connector_right, AND(OR(FLIPPERS, PEGASUS_BOOTS), FEATHER)) # jump the gap in underground passage to d6 left side to skip hookshot - bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue + if not options.rooster: + bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue fire_cave_bottom.connect(fire_cave_top, PEGASUS_BOOTS, one_way=True) # flame skip if options.logic == 'glitched' or options.logic == 'hell': @@ -502,9 +507,9 @@ def __init__(self, options, world_setup, r): tiny_island.connect(left_bay_area, AND(FEATHER, r.bush)) # jesus jump around bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up - + ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze - fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook + fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook animal_village.connect(ukuku_prairie, FEATHER) # jesus jump below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth) animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off @@ -519,12 +524,12 @@ def __init__(self, options, world_setup, r): obstacle_cave_inside_chest.connect(obstacle_cave_inside, FEATHER) # jump to the rightmost pits + 1 pit buffer to jump across obstacle_cave_exit.connect(obstacle_cave_inside, FEATHER) # 1 pit buffer above boots crystals to get past lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, BOMB), one_way=True) # bomb trigger papahl from below ledge, requires pineapple - + self._addEntranceRequirement("heartpiece_swim_cave", FEATHER) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below self._addEntranceRequirement("mambo", FEATHER) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance outside_raft_house.connect(below_right_taltal, FEATHER, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south - self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain + self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain outside_rooster_house.connect(lower_right_taltal, FEATHER) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain d7_platau.connect(water_cave_hole, None, one_way=True) # use save and quit menu to gain control while falling to dodge the water cave hole mountain_bridge_staircase.connect(outside_rooster_house, AND(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across @@ -547,7 +552,7 @@ def __init__(self, options, world_setup, r): graveyard.connect(forest, OR(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk witches hut, or hookshot spam across the pit graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT) # hookshot spam over the pit graveyard_cave_right.connect(graveyard_cave_left, PEGASUS_BOOTS, one_way=True) # boots bonk off the cracked block - + self._addEntranceRequirementEnter("mamu", AND(PEGASUS_BOOTS, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall self._addEntranceRequirement("castle_jump_cave", PEGASUS_BOOTS) # pit buffer to clip bottom wall and boots bonk across prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(PEGASUS_BOOTS, HOOKSHOT))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across @@ -563,15 +568,15 @@ def __init__(self, options, world_setup, r): animal_village.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) ukuku_prairie.connect(bay_water, FEATHER, one_way=True) # jesus jump bay_water.connect(d5_entrance, FEATHER) # jesus jump into d5 entrance (wall clip), wall clip + jesus jump to get out - + crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, FEATHER)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) animal_village_bombcave_heartpiece.connect(animal_village_bombcave, PEGASUS_BOOTS) # boots bonk across bottom wall (both at entrance and in item room) d6_armos_island.connect(ukuku_prairie, FEATHER) # jesus jump (3 screen) from seashell mansion to armos island - armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling + armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling d6_connector_right.connect(d6_connector_left, PEGASUS_BOOTS) # boots bonk across bottom wall at water and pits (can do both ways) - + obstacle_cave_entrance.connect(obstacle_cave_inside, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS, OR(SWORD, MAGIC_ROD, BOW)))) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down obstacle_cave_entrance.connect(obstacle_cave_inside, AND(PEGASUS_BOOTS, ROOSTER)) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall d4_entrance.connect(below_right_taltal, FEATHER) # jesus jump a long way @@ -583,7 +588,7 @@ def __init__(self, options, world_setup, r): mountain_bridge_staircase.connect(outside_rooster_house, OR(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(PEGASUS_BOOTS, FEATHER), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area - + self.start = start_house self.egg = windfish_egg self.nightmare = nightmare @@ -659,7 +664,7 @@ def __init__(self, outside, requirement, one_way_enter_requirement="UNSET", one_ self.requirement = requirement self.one_way_enter_requirement = one_way_enter_requirement self.one_way_exit_requirement = one_way_exit_requirement - + def addRequirement(self, new_requirement): self.requirement = OR(self.requirement, new_requirement) @@ -674,9 +679,9 @@ def addEnterRequirement(self, new_requirement): self.one_way_enter_requirement = new_requirement else: self.one_way_enter_requirement = OR(self.one_way_enter_requirement, new_requirement) - + def enterIsSet(self): return self.one_way_enter_requirement != "UNSET" - + def exitIsSet(self): return self.one_way_exit_requirement != "UNSET" diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py index acc969ba938d..a8e57327e78b 100644 --- a/worlds/ladx/LADXR/logic/requirements.py +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -265,6 +265,7 @@ def __init__(self, options): self.rear_attack_range = OR(MAGIC_ROD, BOW) # mimic self.fire = OR(MAGIC_POWDER, MAGIC_ROD) # torches self.push_hardhat = OR(SHIELD, SWORD, HOOKSHOT, BOOMERANG) + self.shuffled_magnifier = TRADING_ITEM_MAGNIFYING_GLASS self.boss_requirements = [ SWORD, # D1 boss @@ -293,6 +294,8 @@ def __init__(self, options): } # Adjust for options + if not options.tradequest: + self.shuffled_magnifier = True if options.bowwow != 'normal': # We cheat in bowwow mode, we pretend we have the sword, as bowwow can pretty much do all what the sword ca$ # Except for taking out bushes (and crystal pillars are removed) self.bush.remove(SWORD) diff --git a/worlds/ladx/LADXR/patches/maptweaks.py b/worlds/ladx/LADXR/patches/maptweaks.py index c25dd83dcada..8a5171b3540d 100644 --- a/worlds/ladx/LADXR/patches/maptweaks.py +++ b/worlds/ladx/LADXR/patches/maptweaks.py @@ -25,3 +25,16 @@ def addBetaRoom(rom): re.store(rom) rom.room_sprite_data_indoor[0x0FC] = rom.room_sprite_data_indoor[0x1A1] + + +def tweakBirdKeyRoom(rom): + # Make the bird key accessible without the rooster + re = RoomEditor(rom, 0x27A) + re.removeObject(1, 6) + re.removeObject(2, 6) + re.removeObject(3, 5) + re.removeObject(3, 6) + re.moveObject(1, 5, 2, 6) + re.moveObject(2, 5, 3, 6) + re.addEntity(3, 5, 0x9D) + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/songs.py b/worlds/ladx/LADXR/patches/songs.py index 59ca01c4c8c4..b080cf06bc92 100644 --- a/worlds/ladx/LADXR/patches/songs.py +++ b/worlds/ladx/LADXR/patches/songs.py @@ -72,6 +72,10 @@ def upgradeMarin(rom): rst 8 """), fill_nop=True) + # Load marin singing even if you have the marin date + rom.patch(0x03, 0x0A91, ASM("jp nz, $3F8D"), "", fill_nop=True) + rom.patch(0x05, 0x0E6E, ASM("jp nz, $7B4B"), "", fill_nop=True) + def upgradeManbo(rom): # Instead of checking if we have the song, check if we have a specific room flag set diff --git a/worlds/ladx/LADXR/patches/tradeSequence.py b/worlds/ladx/LADXR/patches/tradeSequence.py index 5b608977f20d..0eb46ae23ae2 100644 --- a/worlds/ladx/LADXR/patches/tradeSequence.py +++ b/worlds/ladx/LADXR/patches/tradeSequence.py @@ -1,7 +1,7 @@ from ..assembler import ASM -def patchTradeSequence(rom, boomerang_option): +def patchTradeSequence(rom, settings): patchTrendy(rom) patchPapahlsWife(rom) patchYipYip(rom) @@ -16,7 +16,7 @@ def patchTradeSequence(rom, boomerang_option): patchMermaid(rom) patchMermaidStatue(rom) patchSharedCode(rom) - patchVarious(rom, boomerang_option) + patchVarious(rom, settings) patchInventoryMenu(rom) @@ -265,8 +265,11 @@ def patchMermaidStatue(rom): and $10 ; scale ret z ldh a, [$F8] - and $20 + and $20 ; ROOM_STATUS_EVENT_2 ret nz + + ld hl, wTradeSequenceItem2 + res 4, [hl] ; take the trade item """), fill_nop=True) @@ -317,7 +320,7 @@ def patchSharedCode(rom): rom.patch(0x07, 0x3F7F, "00" * 7, ASM("ldh a, [$F8]\nor $20\nldh [$F8], a\nret")) -def patchVarious(rom, boomerang_option): +def patchVarious(rom, settings): # Make the zora photo work with the magnifier rom.patch(0x18, 0x09F3, 0x0A02, ASM(""" ld a, [wTradeSequenceItem2] @@ -330,22 +333,71 @@ def patchVarious(rom, boomerang_option): jp z, $3F8D ; UnloadEntity """), fill_nop=True) # Mimic invisibility - rom.patch(0x18, 0x2AC8, 0x2ACE, "", fill_nop=True) + rom.patch(0x19, 0x2AC0, ASM(""" + cp $97 + jr z, mermaidStatueCave + cp $98 + jr nz, visible + mermaidStatueCave: + ld a, [$DB7F] + and a + jr nz, 6 + visible: + """), ASM(""" + dec a ; save one byte by only doing one cp + or $01 + cp $97 + jr nz, visible + mermaidStatueCave: + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jr z, 6 + visible: + """)) + # Zol invisibility + rom.patch(0x06, 0x3BE9, ASM(""" + cp $97 + jr z, mermaidStatueCave + cp $98 + ret nz ; visible + mermaidStatueCave: + ld a, [$DB7F] + and a + ret z + """), ASM(""" + dec a ; save one byte by only doing one cp + or $01 + cp $97 + ret nz ; visible + mermaidStatueCave: + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + ret nz + """)) # Ignore trade quest state for marin at beach rom.patch(0x18, 0x219E, 0x21A6, "", fill_nop=True) # Shift the magnifier 8 pixels rom.patch(0x03, 0x0F68, 0x0F6F, ASM(""" ldh a, [$F6] ; map room - cp $97 ; check if we are in the maginfier room + cp $97 ; check if we are in the magnifier room jp z, $4F83 """), fill_nop=True) # Something with the photographer rom.patch(0x36, 0x0948, 0x0950, "", fill_nop=True) - if boomerang_option not in {'trade', 'gift'}: # Boomerang cave is not patched, so adjust it + # Boomerang trade guy + # if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}: + if settings.tradequest: + # Update magnifier checks rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout - rom.patch(0x19, 0x05F4, 0x05FB, "", fill_nop=True) + else: + # Monkey bridge patch, always have the bridge there. + rom.patch(0x00, 0x333D, ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) + # Always have the boomerang trade guy enabled (magnifier not needed) + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout + rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True) def patchInventoryMenu(rom): diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index c5dcc080537c..6c0b866b5071 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -58,7 +58,7 @@ class TextShuffle(DefaultOffToggle): class Rooster(DefaultOnToggle, LADXROption): """ [On] Adds the rooster to the item pool. - [Off] The rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means. + [Off] The rooster spot is still a check giving an item. But you will never find the rooster. In that case, any rooster spot is accessible without rooster by other means. """ display_name = "Rooster" ladxr_name = "rooster" From 7b755408fa41e72b4ecf0f1e9029fe0c21ab85ee Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Sat, 30 Nov 2024 20:00:06 -0800 Subject: [PATCH 071/112] DS3: Clarify location names for Yoel and Yuria items (#3881) * DS3: Clarify location names for Yoel and Yuria items * Fix encodings for `detailed_location_descriptions.py` * Fix one more typo --- worlds/dark_souls_3/Bosses.py | 8 ++-- worlds/dark_souls_3/Locations.py | 28 +++++------ .../detailed_location_descriptions.py | 6 ++- worlds/dark_souls_3/docs/locations_en.md | 48 +++++++++---------- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/worlds/dark_souls_3/Bosses.py b/worlds/dark_souls_3/Bosses.py index fac7d913c338..ce2ba5d1700e 100644 --- a/worlds/dark_souls_3/Bosses.py +++ b/worlds/dark_souls_3/Bosses.py @@ -253,10 +253,10 @@ class DS3BossInfo: }), DS3BossInfo("Lords of Cinder", 4100800, locations = { "KFF: Soul of the Lords", - "FS: Billed Mask - Yuria after killing KFF boss", - "FS: Black Dress - Yuria after killing KFF boss", - "FS: Black Gauntlets - Yuria after killing KFF boss", - "FS: Black Leggings - Yuria after killing KFF boss" + "FS: Billed Mask - shop after killing Yuria", + "FS: Black Dress - shop after killing Yuria", + "FS: Black Gauntlets - shop after killing Yuria", + "FS: Black Leggings - shop after killing Yuria" }), ] diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index 08f4b7cd1a80..cc202c76e8be 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -764,29 +764,29 @@ def __init__( DS3LocationData("US -> RS", None), # Yoel/Yuria of Londor - DS3LocationData("FS: Soul Arrow - Yoel/Yuria", "Soul Arrow", + DS3LocationData("FS: Soul Arrow - Yoel/Yuria shop", "Soul Arrow", static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, shop=True), - DS3LocationData("FS: Heavy Soul Arrow - Yoel/Yuria", "Heavy Soul Arrow", + DS3LocationData("FS: Heavy Soul Arrow - Yoel/Yuria shop", "Heavy Soul Arrow", static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, shop=True), - DS3LocationData("FS: Magic Weapon - Yoel/Yuria", "Magic Weapon", + DS3LocationData("FS: Magic Weapon - Yoel/Yuria shop", "Magic Weapon", static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, shop=True), - DS3LocationData("FS: Magic Shield - Yoel/Yuria", "Magic Shield", + DS3LocationData("FS: Magic Shield - Yoel/Yuria shop", "Magic Shield", static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, shop=True), - DS3LocationData("FS: Soul Greatsword - Yoel/Yuria", "Soul Greatsword", + DS3LocationData("FS: Soul Greatsword - Yoel/Yuria shop", "Soul Greatsword", static='99,0:-1:50000,110000,70000450,70000475:', missable=True, npc=True, shop=True), - DS3LocationData("FS: Dark Hand - Yoel/Yuria", "Dark Hand", missable=True, npc=True), - DS3LocationData("FS: Untrue White Ring - Yoel/Yuria", "Untrue White Ring", missable=True, + DS3LocationData("FS: Dark Hand - Yuria shop", "Dark Hand", missable=True, npc=True), + DS3LocationData("FS: Untrue White Ring - Yuria shop", "Untrue White Ring", missable=True, npc=True), - DS3LocationData("FS: Untrue Dark Ring - Yoel/Yuria", "Untrue Dark Ring", missable=True, + DS3LocationData("FS: Untrue Dark Ring - Yuria shop", "Untrue Dark Ring", missable=True, npc=True), - DS3LocationData("FS: Londor Braille Divine Tome - Yoel/Yuria", "Londor Braille Divine Tome", + DS3LocationData("FS: Londor Braille Divine Tome - Yuria shop", "Londor Braille Divine Tome", static='99,0:-1:40000,110000,70000116:', missable=True, npc=True), - DS3LocationData("FS: Darkdrift - Yoel/Yuria", "Darkdrift", missable=True, drop=True, + DS3LocationData("FS: Darkdrift - kill Yuria", "Darkdrift", missable=True, drop=True, npc=True), # kill her or kill Soul of Cinder # Cornyx of the Great Swamp @@ -2476,13 +2476,13 @@ def __init__( "Firelink Leggings", boss=True, shop=True), # Yuria (quest, after Soul of Cinder) - DS3LocationData("FS: Billed Mask - Yuria after killing KFF boss", "Billed Mask", + DS3LocationData("FS: Billed Mask - shop after killing Yuria", "Billed Mask", missable=True, npc=True), - DS3LocationData("FS: Black Dress - Yuria after killing KFF boss", "Black Dress", + DS3LocationData("FS: Black Dress - shop after killing Yuria", "Black Dress", missable=True, npc=True), - DS3LocationData("FS: Black Gauntlets - Yuria after killing KFF boss", "Black Gauntlets", + DS3LocationData("FS: Black Gauntlets - shop after killing Yuria", "Black Gauntlets", missable=True, npc=True), - DS3LocationData("FS: Black Leggings - Yuria after killing KFF boss", "Black Leggings", + DS3LocationData("FS: Black Leggings - shop after killing Yuria", "Black Leggings", missable=True, npc=True), ], diff --git a/worlds/dark_souls_3/detailed_location_descriptions.py b/worlds/dark_souls_3/detailed_location_descriptions.py index e20c700ab1bc..6e6cf1eb0bc8 100644 --- a/worlds/dark_souls_3/detailed_location_descriptions.py +++ b/worlds/dark_souls_3/detailed_location_descriptions.py @@ -84,7 +84,11 @@ table += f"{html.escape(name)}{html.escape(description)}\n" table += "\n" - with open(os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), 'r+') as f: + with open( + os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), + 'r+', + encoding='utf-8' + ) as f: original = f.read() start_flag = "\n" start = original.index(start_flag) + len(start_flag) diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md index ef07b84b2b34..8411b8c42aa0 100644 --- a/worlds/dark_souls_3/docs/locations_en.md +++ b/worlds/dark_souls_3/docs/locations_en.md @@ -1020,7 +1020,7 @@ static _Dark Souls III_ randomizer]. CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner CKG: Estus Shard - balconyFrom the middle level of the first Consumed King's Gardens elevator, out the balcony and to the right -CKG: Human Pine Resin - by lone stairway bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, in a toxic pool +CKG: Human Pine Resin - pool by liftOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, in a toxic pool CKG: Human Pine Resin - toxic pool, past rotundaIn between two platforms near the middle of the garden, by a tree in a toxic pool CKG: Magic Stoneplate Ring - mob drop before bossDropped by the Cathedral Knight closest to the Oceiros fog gate CKG: Ring of Sacrifice - under balconyAlong the right wall of the garden, next to the first elevator building @@ -1181,16 +1181,18 @@ static _Dark Souls III_ randomizer]. FS: Alluring Skull - Mortician's AshesSold by Handmaid after giving Mortician's Ashes FS: Arstor's Spear - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood FS: Aural Decoy - OrbeckSold by Orbeck -FS: Billed Mask - Yuria after killing KFF bossDropped by Yuria upon death or quest completion. -FS: Black Dress - Yuria after killing KFF bossDropped by Yuria upon death or quest completion. +FS: Billed Mask - shop after killing YuriaDropped by Yuria upon death or quest completion. +FS: Black Dress - shop after killing YuriaDropped by Yuria upon death or quest completion. FS: Black Fire Orb - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome FS: Black Flame - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome -FS: Black Gauntlets - Yuria after killing KFF bossDropped by Yuria upon death or quest completion. +FS: Black Gauntlets - shop after killing YuriaDropped by Yuria upon death or quest completion. +FS: Black Hand Armor - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai +FS: Black Hand Hat - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai FS: Black Iron Armor - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake FS: Black Iron Gauntlets - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake FS: Black Iron Helm - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake FS: Black Iron Leggings - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake -FS: Black Leggings - Yuria after killing KFF bossDropped by Yuria upon death or quest completion. +FS: Black Leggings - shop after killing YuriaDropped by Yuria upon death or quest completion. FS: Black Serpent - Ludleth for WolnirBoss weapon for High Lord Wolnir FS: Blessed Weapon - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric FS: Blue Tearstone Ring - GreiratGiven by Greirat upon rescuing him from the High Wall cell @@ -1220,8 +1222,8 @@ static _Dark Souls III_ randomizer]. FS: Dancer's Leggings - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley FS: Dark Blade - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome FS: Dark Edge - KarlaSold by Karla after recruiting her, or in her ashes -FS: Dark Hand - Yoel/YuriaSold by Yuria -FS: Darkdrift - Yoel/YuriaDropped by Yuria upon death or quest completion. +FS: Dark Hand - Yuria shopSold by Yuria +FS: Darkdrift - kill YuriaDropped by Yuria upon death or quest completion. FS: Darkmoon Longbow - Ludleth for AldrichBoss weapon for Aldrich FS: Dead Again - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome FS: Deep Protection - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome @@ -1264,6 +1266,9 @@ static _Dark Souls III_ randomizer]. FS: Exile Gauntlets - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep FS: Exile Leggings - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep FS: Exile Mask - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep +FS: Faraam Armor - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert +FS: Faraam Boots - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert +FS: Faraam Gauntlets - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert FS: Faraam Helm - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert FS: Farron Dart - OrbeckSold by Orbeck FS: Farron Dart - shopSold by Handmaid @@ -1308,7 +1313,7 @@ static _Dark Souls III_ randomizer]. FS: Heal - IrinaSold by Irina after recruiting her, or in her ashes FS: Heal Aid - shopSold by Handmaid FS: Heavy Soul Arrow - OrbeckSold by Orbeck -FS: Heavy Soul Arrow - Yoel/YuriaSold by Yoel/Yuria +FS: Heavy Soul Arrow - Yoel/Yuria shopSold by Yoel/Yuria FS: Helm of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve FS: Hidden Blessing - Dreamchaser's AshesSold by Greirat after pillaging Irithyll FS: Hidden Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll @@ -1338,7 +1343,7 @@ static _Dark Souls III_ randomizer]. FS: Lift Chamber Key - LeonhardGiven by Ringfinger Leonhard after acquiring a Pale Tongue. FS: Lightning Storm - Ludleth for NamelessBoss weapon for Nameless King FS: Lloyd's Shield Ring - Paladin's AshesSold by Handmaid after giving Paladin's Ashes -FS: Londor Braille Divine Tome - Yoel/YuriaSold by Yuria +FS: Londor Braille Divine Tome - Yuria shopSold by Yuria FS: Lorian's Armor - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince FS: Lorian's Gauntlets - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince FS: Lorian's Greatsword - Ludleth for PrincesBoss weapon for Twin Princes @@ -1347,9 +1352,9 @@ static _Dark Souls III_ randomizer]. FS: Lothric's Holy Sword - Ludleth for PrincesBoss weapon for Twin Princes FS: Magic Barrier - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric FS: Magic Shield - OrbeckSold by Orbeck -FS: Magic Shield - Yoel/YuriaSold by Yoel/Yuria +FS: Magic Shield - Yoel/Yuria shopSold by Yoel/Yuria FS: Magic Weapon - OrbeckSold by Orbeck -FS: Magic Weapon - Yoel/YuriaSold by Yoel/Yuria +FS: Magic Weapon - Yoel/Yuria shopSold by Yoel/Yuria FS: Mail Breaker - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton. FS: Master's Attire - NPC dropDropped by Sword Master FS: Master's Gloves - NPC dropDropped by Sword Master @@ -1401,10 +1406,10 @@ static _Dark Souls III_ randomizer]. FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions FS: Soothing Sunlight - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley FS: Soul Arrow - OrbeckSold by Orbeck -FS: Soul Arrow - Yoel/YuriaSold by Yoel/Yuria +FS: Soul Arrow - Yoel/Yuria shopSold by Yoel/Yuria FS: Soul Arrow - shopSold by Handmaid FS: Soul Greatsword - OrbeckSold by Orbeck -FS: Soul Greatsword - Yoel/YuriaSold by Yoel/Yuria after using Draw Out True Strength +FS: Soul Greatsword - Yoel/Yuria shopSold by Yoel/Yuria after using Draw Out True Strength FS: Soul Spear - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll FS: Soul of a Deserted Corpse - bell tower doorNext to the door requiring the Tower Key FS: Spook - OrbeckSold by Orbeck @@ -1427,8 +1432,8 @@ static _Dark Souls III_ randomizer]. FS: Undead Legion Gauntlet - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers FS: Undead Legion Helm - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers FS: Undead Legion Leggings - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers -FS: Untrue Dark Ring - Yoel/YuriaSold by Yuria -FS: Untrue White Ring - Yoel/YuriaSold by Yuria +FS: Untrue Dark Ring - Yuria shopSold by Yuria +FS: Untrue White Ring - Yuria shopSold by Yuria FS: Vordt's Great Hammer - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley FS: Vow of Silence - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome FS: Washing Pole - Easterner's AshesSold by Handmaid after giving Easterner's Ashes @@ -1477,8 +1482,6 @@ static _Dark Souls III_ randomizer]. FSBT: Twinkling Titanite - lizard behind FirelinkDropped by the Crystal Lizard behind Firelink Shrine. Can be accessed with tree jump by going all the way around the roof, left of the entrance to the rafters, or alternatively dropping down from the Bell Tower. FSBT: Very good! Carving - crow for Divine BlessingTrade Divine Blessing with crow GA: Avelyn - 1F, drop from 3F onto bookshelvesOn top of a bookshelf on the Archive first floor, accessible by going halfway up the stairs to the third floor, dropping down past the Grand Archives Scholar, and then dropping down again -GA: Black Hand Armor - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai -GA: Black Hand Hat - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai GA: Blessed Gem - raftersOn the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area GA: Chaos Gem - dark room, lizardDropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool GA: Cinders of a Lord - Lothric PrinceDropped by Twin Princes @@ -1489,9 +1492,6 @@ static _Dark Souls III_ randomizer]. GA: Divine Pillars of Light - cage above raftersIn a cage above the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area GA: Ember - 5F, by entranceOn a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, on the opposite side from the wax pool GA: Estus Shard - dome, far balconyOn the Archives roof near the three Winged Knights, in a side area overlooking the ocean. -GA: Faraam Armor - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert -GA: Faraam Boots - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert -GA: Faraam Gauntlets - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert GA: Fleshbite Ring - up stairs from 4FFrom the first shortcut elevator with the movable bookshelf, past the Scholars right before going outside onto the roof, in an alcove to the right with many Clawed Curse bookshelves GA: Golden Wing Crest Shield - outside 5F, NPC dropDropped by Lion Knight Albert before the stairs leading up to Twin Princes GA: Heavy Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof @@ -1525,15 +1525,15 @@ static _Dark Souls III_ randomizer]. GA: Titanite Chunk - 2F, by wax poolUp the stairs from the Archives second floor on the right side from the entrance, in a corner near the small wax pool GA: Titanite Chunk - 2F, right after dark roomExiting from the dark room with the Crystal Lizards on the first floor onto the second floor main room, then taking an immediate right GA: Titanite Chunk - 5F, far balconyOn a balcony outside where Lothric Knight stands on the top floor of the Archives, accessing by going right from the final wax pool or by dropping down from the gargoyle area -GA: Titanite Chunk - rooftops, balconyGoing onto the roof and down the first ladder, all the way down the ledge facing the ocean to the right GA: Titanite Chunk - rooftops lower, ledge by buttressGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, on a roof ledge to the right +GA: Titanite Chunk - rooftops, balconyGoing onto the roof and down the first ladder, all the way down the ledge facing the ocean to the right GA: Titanite Chunk - rooftops, just before 5FOn the Archives roof, after a short dropdown, in the small area where the two Gargoyles attack you GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizardDropped by a Crystal Lizard on first floor bookshelves. Can be accessed by dropping down to the left at the end of the bridge which is the Crystal Sage's final location GA: Titanite Scale - 1F, up stairs on bookshelfOn the Archives first floor, up a movable set of stairs near the large wax pool, on top of a bookshelf GA: Titanite Scale - 2F, titanite scale atop bookshelfOn top of a bookshelf on the Archive second floor, accessible by going halfway up the stairs to the third floor and dropping down near a Grand Archives Scholar GA: Titanite Scale - 3F, by ladder to 2F lateGoing from the Crystal Sage's location on the third floor to its location on the bridge, on the left side of the ladder you descend, behind a table GA: Titanite Scale - 3F, corner up stairsFrom the Grand Archives third floor up past the thralls, in a corner with bookshelves to the left -GA: Titanite Scale - 5F, chest by exitIn a chest after the first elevator shortcut with the movable bookshelf, in the area with the Grand Archives Scholars, to the left of the stairwell leading up to the roof +GA: Titanite Scale - 4F, chest by exitIn a chest after the first elevator shortcut with the movable bookshelf, in the area with the Grand Archives Scholars, to the left of the stairwell leading up to the roof GA: Titanite Scale - dark room, upstairsRight after going up the stairs to the Archives second floor, on the left guarded by a Grand Archives Scholar and a sequence of Clawed Curse bookshelves GA: Titanite Scale - rooftops lower, path to 2FGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, then going past the corvians all the way to the left and making a jump GA: Titanite Slab - 1F, after pulling 2F switchIn a chest on the Archives first floor, behind a bookshelf moved by pulling a lever in the middle of the second floor between two cursed bookshelves @@ -1633,7 +1633,7 @@ static _Dark Souls III_ randomizer]. IBV: Large Soul of a Nameless Soldier - central, by bonfireBy the Central Irithyll bonfire IBV: Large Soul of a Nameless Soldier - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire IBV: Large Soul of a Nameless Soldier - lake islandOn an island in the lake leading to the Distant Manor bonfire -IBV: Large Soul of a Nameless Soldier - stairs to plazaOn the path from Central Irithyll bonfire, before making the left toward Church of Yorshka +IBV: Large Soul of a Nameless Soldier - path to plazaOn the path from Central Irithyll bonfire, before making the left toward Church of Yorshka IBV: Large Titanite Shard - Distant Manor, under overhangUnder overhang next to second set of stairs leading from Distant Manor bonfire IBV: Large Titanite Shard - ascent, by elevator doorOn the path from the sewer leading up to Pontiff's cathedral, to the right of the statue surrounded by dogs IBV: Large Titanite Shard - ascent, down ladder in last buildingOutside the final building before Pontiff's cathedral, coming from the sewer, dropping down to the left before the entrance @@ -1701,7 +1701,7 @@ static _Dark Souls III_ randomizer]. ID: Large Titanite Shard - B1 far, rightmost cellIn a cell on the far end of the top corridor opposite to the bonfire in Irithyll Dungeon, nearby the Jailer ID: Large Titanite Shard - B1 near, by doorAt the end of the top corridor on the bonfire side in Irithyll Dungeon, before the Jailbreaker's Key door ID: Large Titanite Shard - B3 near, right cornerIn the main Jailer cell block, to the left of the hallway leading to the Path of the Dragon area -ID: Large Titanite Shard - after bonfire, second cell on rightIn the second cell on the right after Irithyll Dungeon bonfire +ID: Large Titanite Shard - after bonfire, second cell on leftIn the second cell on the right after Irithyll Dungeon bonfire ID: Large Titanite Shard - pit #1On the floor where the Giant Slave is standing ID: Large Titanite Shard - pit #2On the floor where the Giant Slave is standing ID: Lightning Blade - B3 lift, middle platformOn the middle platform riding the elevator up from the Path of the Dragon area From 62ce42440b703fba2ba544604f51690f388b7f33 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:03:13 -0500 Subject: [PATCH 072/112] Super Metroid: KeyError on invalid item name #4222 --- worlds/sm/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index bf9d6d087edd..160b7e4ec78b 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -313,9 +313,11 @@ def remove(self, state: CollectionState, item: Item) -> bool: return super(SMWorld, self).remove(state, item) def create_item(self, name: str) -> Item: - item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], + item = next((x for x in ItemManager.Items.values() if x.Name == name), None) + if item: + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], player=self.player) + raise KeyError(f"Item {name} for {self.player_name} is invalid.") def get_filler_item_name(self) -> str: if self.multiworld.random.randint(0, 100) < self.options.minor_qty.value: From 284e7797c5523180968016728790bf5128315c61 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:10:43 -0500 Subject: [PATCH 073/112] Adventure: create_item AttributeError -> KeyError #4219 --- worlds/adventure/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index ed5ebbd3dc56..4fde1482cfe1 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -446,7 +446,7 @@ def generate_output(self, output_directory: str) -> None: # end of ordered Main.py calls def create_item(self, name: str) -> Item: - item_data: ItemData = item_table.get(name) + item_data: ItemData = item_table[name] return AdventureItem(name, item_data.classification, item_data.id, self.player) def create_event(self, name: str, classification: ItemClassification) -> Item: From 17b3ee6eaf326249c477b20ae976794817efc3ee Mon Sep 17 00:00:00 2001 From: Benjamin S Wolf Date: Sat, 30 Nov 2024 20:18:00 -0800 Subject: [PATCH 074/112] Core: warn if a yaml is empty (#4117) * Core: warn if a yaml is empty * WebHost: ignore empty yaml Generate: log which yaml documents are empty * Actually remove empty yamls from weight_cache * More verbose variable name --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Generate.py | 9 ++++++++- WebHostLib/check.py | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Generate.py b/Generate.py index 8aba72abafe9..35c39627b139 100644 --- a/Generate.py +++ b/Generate.py @@ -114,7 +114,14 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: - weights_cache[fname] = read_weights_yamls(path) + weights_for_file = [] + for doc_idx, yaml in enumerate(read_weights_yamls(path)): + if yaml is None: + logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}") + else: + weights_for_file.append(yaml) + weights_cache[fname] = tuple(weights_for_file) + except Exception as e: raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 97cb797f7a56..4e0cf1178f4b 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -105,8 +105,9 @@ def roll_options(options: Dict[str, Union[dict, str]], plando_options=plando_options) else: for i, yaml_data in enumerate(yaml_datas): - rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, - plando_options=plando_options) + if yaml_data is not None: + rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, + plando_options=plando_options) except Exception as e: if e.__cause__: results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}" From ecc3094c70b3ee1f3e18d9299c03198564ec261a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 1 Dec 2024 08:33:43 +0100 Subject: [PATCH 075/112] Launcher: launch without delay on URI without choice (#4279) --- Launcher.py | 58 +++++++++++++++++------------------------------------ 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/Launcher.py b/Launcher.py index 0b8be232170d..22c0944ab1a4 100644 --- a/Launcher.py +++ b/Launcher.py @@ -126,12 +126,13 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: elif component.display_name == "Text Client": text_client_component = component - from kvui import App, Button, BoxLayout, Label, Clock, Window + if client_component is None: + run_component(text_client_component, *launch_args) + return - class Popup(App): - timer_label: Label - remaining_time: Optional[int] + from kvui import App, Button, BoxLayout, Label, Window + class Popup(App): def __init__(self): self.title = "Connect to Multiworld" self.icon = r"data/icon.png" @@ -139,48 +140,25 @@ def __init__(self): def build(self): layout = BoxLayout(orientation="vertical") + layout.add_widget(Label(text="Select client to open and connect with.")) + button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) - if client_component is None: - self.remaining_time = 7 - label_text = (f"A game client able to parse URIs was not detected for {game}.\n" - f"Launching Text Client in 7 seconds...") - self.timer_label = Label(text=label_text) - layout.add_widget(self.timer_label) - Clock.schedule_interval(self.update_label, 1) - else: - layout.add_widget(Label(text="Select client to open and connect with.")) - button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) - - text_client_button = Button( - text=text_client_component.display_name, - on_release=lambda *args: run_component(text_client_component, *launch_args) - ) - button_row.add_widget(text_client_button) + text_client_button = Button( + text=text_client_component.display_name, + on_release=lambda *args: run_component(text_client_component, *launch_args) + ) + button_row.add_widget(text_client_button) - game_client_button = Button( - text=client_component.display_name, - on_release=lambda *args: run_component(client_component, *launch_args) - ) - button_row.add_widget(game_client_button) + game_client_button = Button( + text=client_component.display_name, + on_release=lambda *args: run_component(client_component, *launch_args) + ) + button_row.add_widget(game_client_button) - layout.add_widget(button_row) + layout.add_widget(button_row) return layout - def update_label(self, dt): - if self.remaining_time > 1: - # countdown the timer and string replace the number - self.remaining_time -= 1 - self.timer_label.text = self.timer_label.text.replace( - str(self.remaining_time + 1), str(self.remaining_time) - ) - else: - # our timer is finished so launch text client and close down - run_component(text_client_component, *launch_args) - Clock.unschedule(self.update_label) - App.get_running_app().stop() - Window.close() - def _stop(self, *largs): # see run_gui Launcher _stop comment for details self.root_window.close() From f26cda07db5cb9046088a6f0c4710071c7bcfc69 Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Sun, 1 Dec 2024 09:16:36 -0500 Subject: [PATCH 076/112] Core: Hint Priority fixes (#4315) * Update hint priority docs * Update network protocol.md * Add error on `UpdateHint` trying to change to `HINT_FOUND` * Update network protocol.md * fix: precollected hint priority --- Main.py | 14 +++++++++----- MultiServer.py | 5 +++++ docs/network protocol.md | 17 +++++++++++------ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Main.py b/Main.py index 3a11181bd99f..d105bd4ad0e5 100644 --- a/Main.py +++ b/Main.py @@ -242,6 +242,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No def write_multidata(): import NetUtils + from NetUtils import HintStatus slot_data = {} client_versions = {} games = {} @@ -266,10 +267,10 @@ def write_multidata(): for slot in multiworld.player_ids: slot_data[slot] = multiworld.worlds[slot].fill_slot_data() - def precollect_hint(location): + def precollect_hint(location: Location, auto_status: HintStatus): entrance = er_hint_data.get(location.player, {}).get(location.address, "") hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False, entrance, location.item.flags, False) + location.item.code, False, entrance, location.item.flags, auto_status) precollected_hints[location.player].add(hint) if location.item.player not in multiworld.groups: precollected_hints[location.item.player].add(hint) @@ -288,13 +289,16 @@ def precollect_hint(location): f"{locations_data[location.player][location.address]}") locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags + auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY if location.name in multiworld.worlds[location.player].options.start_location_hints: - precollect_hint(location) + if not location.item.trap: # Unspecified status for location hints, except traps + auto_status = HintStatus.HINT_UNSPECIFIED + precollect_hint(location, auto_status) elif location.item.name in multiworld.worlds[location.item.player].options.start_hints: - precollect_hint(location) + precollect_hint(location, auto_status) elif any([location.item.name in multiworld.worlds[player].options.start_hints for player in multiworld.groups.get(location.item.player, {}).get("players", [])]): - precollect_hint(location) + precollect_hint(location, auto_status) # embedded data package data_package = { diff --git a/MultiServer.py b/MultiServer.py index 80fcd32fd1e3..2561b0692a3c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1929,6 +1929,11 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: Invalid Status', "original_cmd": cmd}]) return + if status == HintStatus.HINT_FOUND: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}]) + return new_hint = new_hint.re_prioritize(ctx, status) if hint == new_hint: return diff --git a/docs/network protocol.md b/docs/network protocol.md index 1c5b2e002289..4331cf971007 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -351,7 +351,7 @@ Sent to the server to update the status of a Hint. The client must be the 'recei | ---- | ---- | ----- | | player | int | The ID of the player whose location is being hinted for. | | location | int | The ID of the location to update the hint for. If no hint exists for this location, the packet is ignored. | -| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. | +| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. Cannot set `HINT_FOUND`, or change the status from `HINT_FOUND`. | #### HintStatus An enumeration containing the possible hint states. @@ -359,12 +359,16 @@ An enumeration containing the possible hint states. ```python import enum class HintStatus(enum.IntEnum): - HINT_FOUND = 0 - HINT_UNSPECIFIED = 1 - HINT_NO_PRIORITY = 10 - HINT_AVOID = 20 - HINT_PRIORITY = 30 + HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found. + HINT_UNSPECIFIED = 1 # The receiving player has not specified any status + HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded + HINT_AVOID = 20 # The receiving player has specified that the item is detrimental + HINT_PRIORITY = 30 # The receiving player has specified that the item is needed ``` +- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`. +- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`. +- Hints created with `!hint` or similar (hinting an item for yourself) default to `HINT_PRIORITY`. +- Once a hint is collected, its' status is updated to `HINT_FOUND` automatically, and can no longer be changed. ### StatusUpdate Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past) @@ -668,6 +672,7 @@ class Hint(typing.NamedTuple): found: bool entrance: str = "" item_flags: int = 0 + status: HintStatus = HintStatus.HINT_UNSPECIFIED ``` ### Data Package Contents From b6ab91fe4b5491c1d754aba35885f7b6e1923263 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 2 Dec 2024 20:50:30 -0500 Subject: [PATCH 077/112] LADX: Remove duplicate Magnifying Lens item (#3684) * LADX: Magnifying Glass fixes Removed the duplicate item (Magnifying Lens), and made the real one a filler item. * Update worlds/ladx/Items.py Co-authored-by: threeandthreee --------- Co-authored-by: threeandthreee --- worlds/ladx/Items.py | 2 -- worlds/ladx/LADXR/locations/constants.py | 2 +- worlds/ladx/LADXR/locations/items.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index 1f9358a4f5a6..b9e1eeab3e69 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -69,7 +69,6 @@ class ItemName: BOMB = "Bomb" SWORD = "Progressive Sword" FLIPPERS = "Flippers" - MAGNIFYING_LENS = "Magnifying Lens" MEDICINE = "Medicine" TAIL_KEY = "Tail Key" ANGLER_KEY = "Angler Key" @@ -191,7 +190,6 @@ class ItemName: ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression), ItemData(ItemName.SWORD, "SWORD", ItemClassification.progression), ItemData(ItemName.FLIPPERS, "FLIPPERS", ItemClassification.progression), - ItemData(ItemName.MAGNIFYING_LENS, "MAGNIFYING_LENS", ItemClassification.progression), ItemData(ItemName.MEDICINE, "MEDICINE", ItemClassification.useful), ItemData(ItemName.TAIL_KEY, "TAIL_KEY", ItemClassification.progression), ItemData(ItemName.ANGLER_KEY, "ANGLER_KEY", ItemClassification.progression), diff --git a/worlds/ladx/LADXR/locations/constants.py b/worlds/ladx/LADXR/locations/constants.py index 7bb8df5b3515..a0489febc316 100644 --- a/worlds/ladx/LADXR/locations/constants.py +++ b/worlds/ladx/LADXR/locations/constants.py @@ -25,7 +25,7 @@ PEGASUS_BOOTS: 0x05, OCARINA: 0x06, FEATHER: 0x07, SHOVEL: 0x08, MAGIC_POWDER: 0x09, BOMB: 0x0A, SWORD: 0x0B, FLIPPERS: 0x0C, - MAGNIFYING_LENS: 0x0D, MEDICINE: 0x10, + MEDICINE: 0x10, TAIL_KEY: 0x11, ANGLER_KEY: 0x12, FACE_KEY: 0x13, BIRD_KEY: 0x14, GOLD_LEAF: 0x15, RUPEES_50: 0x1B, RUPEES_20: 0x1C, RUPEES_100: 0x1D, RUPEES_200: 0x1E, RUPEES_500: 0x1F, SEASHELL: 0x20, MESSAGE: 0x21, GEL: 0x22, diff --git a/worlds/ladx/LADXR/locations/items.py b/worlds/ladx/LADXR/locations/items.py index 50186ef2a34c..1ecc331f8580 100644 --- a/worlds/ladx/LADXR/locations/items.py +++ b/worlds/ladx/LADXR/locations/items.py @@ -11,7 +11,6 @@ BOMB = "BOMB" SWORD = "SWORD" FLIPPERS = "FLIPPERS" -MAGNIFYING_LENS = "MAGNIFYING_LENS" MEDICINE = "MEDICINE" TAIL_KEY = "TAIL_KEY" ANGLER_KEY = "ANGLER_KEY" From 81b9a53a376df3f55f76abe941796fc7aef69e89 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 3 Dec 2024 01:51:10 +0000 Subject: [PATCH 078/112] KH2: Add missing indirect conditions for Final region access (#3923) * KH2: Add missing indirect conditions for Final region access Entrances to the Final region require being able to reach any one of a number of locations, but for a location to be reachable, its parent region must also be reachable, so indirect conditions must be added for these regions. * Use World.get_location Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Use World.get_location, for real this time --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/kh2/Rules.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 4370ad36b540..0f26b56d0e54 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -355,6 +355,16 @@ def __init__(self, world: KH2World) -> None: RegionName.Master: lambda state: self.multi_form_region_access(), RegionName.Final: lambda state: self.final_form_region_access(state) } + # Accessing Final requires being able to reach one of the locations in final_leveling_access, but reaching a + # location requires being able to reach the region the location is in, so an indirect condition is required. + # The access rules of each of the locations in final_leveling_access do not check for being able to reach other + # locations or other regions, so it is only the parent region of each location that needs to be added as an + # indirect condition. + self.form_region_indirect_condition_regions = { + RegionName.Final: { + self.world.get_location(location).parent_region for location in final_leveling_access + } + } def final_form_region_access(self, state: CollectionState) -> bool: """ @@ -388,12 +398,15 @@ def set_kh2_form_rules(self): for region_name in drive_form_list: if region_name == RegionName.Summon and not self.world.options.SummonLevelLocationToggle: continue + indirect_condition_regions = self.form_region_indirect_condition_regions.get(region_name, ()) # could get the location of each of these, but I feel like that would be less optimal region = self.multiworld.get_region(region_name, self.player) # if region_name in form_region_rules if region_name != RegionName.Summon: for entrance in region.entrances: entrance.access_rule = self.form_region_rules[region_name] + for indirect_condition_region in indirect_condition_regions: + self.multiworld.register_indirect_condition(indirect_condition_region, entrance) for loc in region.locations: loc.access_rule = self.form_rules[loc.name] From 18e8d50768eff7ac6416048fd1d39b40551793b7 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 2 Dec 2024 17:52:20 -0800 Subject: [PATCH 079/112] Pokemon Emerald: Clean up dexsanity spoiler and hints (#3832) * Pokemon Emerald: Clean up dexsanity spoiler and hints * Pokemon Emerald: Add +, do less hacks * Pokemon Emerald: Update changelog * Pokemon Emerald: Replace arrow with word in changelog * Pokemon Emerald: Fix changelog --- worlds/pokemon_emerald/CHANGELOG.md | 1 - worlds/pokemon_emerald/__init__.py | 53 ++++++++++++++++++----------- worlds/pokemon_emerald/data.py | 31 +++++++++++++++++ 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 72005d6f9d3f..0dd874b25029 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -2,7 +2,6 @@ ### Features -- Added many new item and location groups. - Added a Swedish translation of the setup guide. - The client communicates map transitions to any trackers connected to the slot. - Added the player's Normalize Encounter Rates option to slot data for trackers. diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index c99a0c11cdfb..040b89b1af51 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -629,21 +629,34 @@ def write_spoiler(self, spoiler_handle: TextIO): spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n") + slot_to_rod_suffix = { + 0: " (Old Rod)", + 1: " (Old Rod)", + 2: " (Good Rod)", + 3: " (Good Rod)", + 4: " (Good Rod)", + 5: " (Super Rod)", + 6: " (Super Rod)", + 7: " (Super Rod)", + 8: " (Super Rod)", + 9: " (Super Rod)", + } + species_maps = defaultdict(set) for map in self.modified_maps.values(): if map.land_encounters is not None: for encounter in map.land_encounters.slots: - species_maps[encounter].add(map.name[4:]) + species_maps[encounter].add(map.label + " (Land)") if map.water_encounters is not None: for encounter in map.water_encounters.slots: - species_maps[encounter].add(map.name[4:]) + species_maps[encounter].add(map.label + " (Water)") if map.fishing_encounters is not None: - for encounter in map.fishing_encounters.slots: - species_maps[encounter].add(map.name[4:]) + for slot, encounter in enumerate(map.fishing_encounters.slots): + species_maps[encounter].add(map.label + slot_to_rod_suffix[slot]) - lines = [f"{emerald_data.species[species].label}: {', '.join(maps)}\n" + lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n" for species, maps in species_maps.items()] lines.sort() for line in lines: @@ -655,35 +668,35 @@ def extend_hint_information(self, hint_data): if self.options.dexsanity: from collections import defaultdict - slot_to_rod = { - 0: "_OLD_ROD", - 1: "_OLD_ROD", - 2: "_GOOD_ROD", - 3: "_GOOD_ROD", - 4: "_GOOD_ROD", - 5: "_SUPER_ROD", - 6: "_SUPER_ROD", - 7: "_SUPER_ROD", - 8: "_SUPER_ROD", - 9: "_SUPER_ROD", + slot_to_rod_suffix = { + 0: " (Old Rod)", + 1: " (Old Rod)", + 2: " (Good Rod)", + 3: " (Good Rod)", + 4: " (Good Rod)", + 5: " (Super Rod)", + 6: " (Super Rod)", + 7: " (Super Rod)", + 8: " (Super Rod)", + 9: " (Super Rod)", } species_maps = defaultdict(set) for map in self.modified_maps.values(): if map.land_encounters is not None: for encounter in map.land_encounters.slots: - species_maps[encounter].add(map.name[4:] + "_GRASS") + species_maps[encounter].add(map.label + " (Land)") if map.water_encounters is not None: for encounter in map.water_encounters.slots: - species_maps[encounter].add(map.name[4:] + "_WATER") + species_maps[encounter].add(map.label + " (Water)") if map.fishing_encounters is not None: for slot, encounter in enumerate(map.fishing_encounters.slots): - species_maps[encounter].add(map.name[4:] + slot_to_rod[slot]) + species_maps[encounter].add(map.label + slot_to_rod_suffix[slot]) hint_data[self.player] = { - self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(maps) + self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps)) for species, maps in species_maps.items() } diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index c7af5ef2284a..d93ff926229b 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -151,6 +151,7 @@ class EncounterTableData(NamedTuple): @dataclass class MapData: name: str + label: str header_address: int land_encounters: Optional[EncounterTableData] water_encounters: Optional[EncounterTableData] @@ -357,6 +358,8 @@ def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]: def _init() -> None: + import re + extracted_data: Dict[str, Any] = load_json_data("extracted_data.json") data.constants = extracted_data["constants"] data.ram_addresses = extracted_data["misc_ram_addresses"] @@ -366,6 +369,7 @@ def _init() -> None: # Create map data for map_name, map_json in extracted_data["maps"].items(): + assert isinstance(map_name, str) if map_name in IGNORABLE_MAPS: continue @@ -389,8 +393,35 @@ def _init() -> None: map_json["fishing_encounters"]["address"] ) + # Derive a user-facing label + label = [] + for word in map_name[4:].split("_"): + # 1F, B1F, 2R, etc. + re_match = re.match("^B?\d+[FRP]$", word) + if re_match: + label.append(word) + continue + + # Route 103, Hall 1, House 5, etc. + re_match = re.match("^([A-Z]+)(\d+)$", word) + if re_match: + label.append(re_match.group(1).capitalize()) + label.append(re_match.group(2).lstrip("0")) + continue + + if word == "OF": + label.append("of") + continue + + if word == "SS": + label.append("S.S.") + continue + + label.append(word.capitalize()) + data.maps[map_name] = MapData( map_name, + " ".join(label), map_json["header_address"], land_encounters, water_encounters, From ffe0221deb41d93b5a2e5a663a43e96beec058db Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 3 Dec 2024 03:00:56 +0100 Subject: [PATCH 080/112] Core: log process ID (#4290) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index 4f99d26ac402..50adb18f42be 100644 --- a/Utils.py +++ b/Utils.py @@ -557,7 +557,7 @@ def _cleanup(): import platform logging.info( f"Archipelago ({__version__}) logging initialized" - f" on {platform.platform()}" + f" on {platform.platform()} process {os.getpid()}" f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" f"{' (frozen)' if is_frozen() else ''}" ) From 6f2e1c2a7ea8395c1674124c1c7e51ffbd1bb7d9 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Mon, 2 Dec 2024 21:02:18 -0500 Subject: [PATCH 081/112] Lingo: Optimize imports and remove unused parameter (#4305) --- worlds/lingo/player_logic.py | 4 ++-- worlds/lingo/utils/pickle_static_data.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b21735c1f533..83217d7311a3 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -412,7 +412,7 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors] - def is_req_enterable(painting_id: str, painting: Painting) -> bool: + def is_req_enterable(painting: Painting) -> bool: if painting.exit_only or painting.disable or painting.req_blocked\ or painting.room in required_painting_rooms: return False @@ -433,7 +433,7 @@ def is_req_enterable(painting_id: str, painting: Painting) -> bool: return True req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() - if is_req_enterable(painting_id, painting)] + if is_req_enterable(painting)] req_exits += [painting_id for painting_id, painting in PAINTINGS.items() if painting.exit_only and painting.required] req_entrances = world.random.sample(req_enterable, len(req_exits)) diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py index 92bcb7a859ea..cd5c4b41df4b 100644 --- a/worlds/lingo/utils/pickle_static_data.py +++ b/worlds/lingo/utils/pickle_static_data.py @@ -11,7 +11,6 @@ import hashlib import pickle -import sys import Utils From 6896d631db6ab24dfef67538ff7e7a7c18ff560f Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:23:13 -0500 Subject: [PATCH 082/112] Stardew Valley: Fix a bug in equals between Or and And rules #4326 --- worlds/stardew_valley/stardew_rule/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 3e6eb327ea99..af4c3c35330d 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -293,7 +293,7 @@ def __repr__(self): def __eq__(self, other): return (isinstance(other, type(self)) and self.combinable_rules == other.combinable_rules and - self.simplification_state.original_simplifiable_rules == self.simplification_state.original_simplifiable_rules) + self.simplification_state.original_simplifiable_rules == other.simplification_state.original_simplifiable_rules) def __hash__(self): if len(self.combinable_rules) + len(self.simplification_state.original_simplifiable_rules) > 5: From ac8a206d4685e015494dd67f9a9e7ab2411773b3 Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Tue, 3 Dec 2024 00:59:55 -0500 Subject: [PATCH 083/112] LADX: combine warp options (#4325) * combine warp options * fix * fix typo * mark old options as removed --- worlds/ladx/LADXR/generator.py | 6 +++--- worlds/ladx/Options.py | 30 ++++++++++++++---------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index f0f042c67db8..b402b3d88919 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -58,7 +58,7 @@ from .patches import bank34 from .utils import formatText -from ..Options import TrendyGame, Palette +from ..Options import TrendyGame, Palette, Warps from .roomEditor import RoomEditor, Object from .patches.aesthetics import rgb_to_bin, bin_to_rgb @@ -416,8 +416,8 @@ def speed(): for channel in range(3): color[channel] = color[channel] * 31 // 0xbc - if world.options.warp_improvements: - patches.core.addWarpImprovements(rom, world.options.additional_warp_points) + if world.options.warps != Warps.option_vanilla: + patches.core.addWarpImprovements(rom, world.options.warps == Warps.option_improved_additional) palette = world.options.palette if palette != Palette.option_normal: diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 6c0b866b5071..863e80fd036b 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -3,7 +3,7 @@ import os.path import typing import logging -from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup +from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup, Removed from collections import defaultdict import Utils @@ -486,21 +486,18 @@ def to_ladxr_option(self, all_options): return self.ladxr_name, s -class WarpImprovements(DefaultOffToggle): +class Warps(Choice): """ - [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. - [Off] No change + [Improved] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. + [Improved Additional] Improved warps, and adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower. """ - display_name = "Warp Improvements" + display_name = "Warps" + option_vanilla = 0 + option_improved = 1 + option_improved_additional = 2 + default = option_vanilla -class AdditionalWarpPoints(DefaultOffToggle): - """ - [On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower - [Off] No change - """ - display_name = "Additional Warp Points" - ladx_option_groups = [ OptionGroup("Goal Options", [ Goal, @@ -515,8 +512,7 @@ class AdditionalWarpPoints(DefaultOffToggle): ShuffleStoneBeaks ]), OptionGroup("Warp Points", [ - WarpImprovements, - AdditionalWarpPoints, + Warps, ]), OptionGroup("Miscellaneous", [ TradeQuest, @@ -562,8 +558,7 @@ class LinksAwakeningOptions(PerGameCommonOptions): # 'bowwow': Bowwow, # 'overworld': Overworld, link_palette: LinkPalette - warp_improvements: WarpImprovements - additional_warp_points: AdditionalWarpPoints + warps: Warps trendy_game: TrendyGame gfxmod: GfxMod palette: Palette @@ -579,3 +574,6 @@ class LinksAwakeningOptions(PerGameCommonOptions): nag_messages: NagMessages ap_title_screen: APTitleScreen boots_controls: BootsControls + + warp_improvements: Removed + additional_warp_points: Removed From 5b0de6b6c77b76a7f40285565d4f688fb662e412 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:51:58 -0500 Subject: [PATCH 084/112] FFMQ: No Longer Allow Inaccessible Useful Items (#4323) Co-authored-by: Doug Hoskisson --- worlds/ffmq/Regions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index c1d3d619ffaa..4e26be1653a6 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -211,9 +211,12 @@ def stage_set_rules(multiworld): # If there's no enemies, there's no repeatable income sources no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") if multiworld.worlds[player].options.enemies_density == "none"] - if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, - ItemClassification.trap)]) > len([player for player in no_enemies_players if - multiworld.worlds[player].options.accessibility == "minimal"]) * 3): + if ( + len([item for item in multiworld.itempool if item.excludable]) > + len([player + for player in no_enemies_players + if multiworld.worlds[player].options.accessibility != "minimal"]) * 3 + ): for player in no_enemies_players: for location in vendor_locations: if multiworld.worlds[player].options.accessibility == "full": @@ -221,11 +224,8 @@ def stage_set_rules(multiworld): else: multiworld.get_location(location, player).access_rule = lambda state: False else: - # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing - # advancement items so that useful items can be placed. - for player in no_enemies_players: - for location in vendor_locations: - multiworld.get_location(location, player).item_rule = lambda item: not item.advancement + raise Exception(f"Not enough filler/trap items for FFMQ players with full and items accessibility. " + f"Add more items or change the 'Enemies Density' option to something besides 'none'") class FFMQLocation(Location): From f43fa612d502c3b5ed307d97f266e6acc8eaa937 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 4 Dec 2024 05:39:29 +0100 Subject: [PATCH 085/112] The Witness: Another small access rule optimisation #4256 --- worlds/witness/rules.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 323d5943c853..dac1556e46d4 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -246,7 +246,22 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte item_rules_converted = [lambda state: state.has(item, player, count)] else: item_counts = {item_rule.item_name: item_rule.item_count for item_rule in item_rules} - item_rules_converted = [lambda state: state.has_all_counts(item_counts, player)] + # Sort the list by which item you are least likely to have (E.g. last stage of progressive item chains) + sorted_item_list = sorted( + item_counts.keys(), + key=lambda item_name: item_counts[item_name] if ("Progressive" in item_name) else 1.5, + reverse=True + # 1.5 because you are less likely to have a single stage item than one copy of a 2-stage chain + # I did some testing and every part of this genuinely gives a tiiiiny performance boost over not having it! + ) + + if all(item_count == 1 for item_count in item_counts.values()): + # If all counts are one, just use state.has_all + item_rules_converted = [lambda state: state.has_all(sorted_item_list, player)] + else: + # If any count is higher than 1, use state.has_all_counts + sorted_item_counts = {item_name: item_counts[item_name] for item_name in sorted_item_list} + item_rules_converted = [lambda state: state.has_all_counts(sorted_item_counts, player)] return collection_rules + item_rules_converted From 769fbc55a9043f323684f6400424167eed2cea80 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Wed, 4 Dec 2024 02:51:56 -0500 Subject: [PATCH 086/112] HK: Remove unused variables and imports (#4303) * Remove unused variables and imports * Accidental duplication --- worlds/hk/Options.py | 2 +- worlds/hk/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index e17abbb7ae47..02f04ab18eef 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,6 +1,6 @@ import typing import re -from dataclasses import dataclass, make_dataclass +from dataclasses import make_dataclass from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index aede8e59cca5..81d939dcf1ea 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -340,7 +340,7 @@ def _add(item_name: str, location_name: str, randomized: bool): for shop, locations in self.created_multi_locations.items(): for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): - loc = self.create_location(shop) + self.create_location(shop) unfilled_locations += 1 # Balance the pool @@ -356,7 +356,7 @@ def _add(item_name: str, location_name: str, randomized: bool): if shops: for _ in range(additional_shop_items): shop = self.random.choice(shops) - loc = self.create_location(shop) + self.create_location(shop) unfilled_locations += 1 if len(self.created_multi_locations[shop]) >= 16: shops.remove(shop) From 58f22053048b97fd18d07843ea63638657257420 Mon Sep 17 00:00:00 2001 From: nmorale5 <76963132+nmorale5@users.noreply.github.com> Date: Thu, 5 Dec 2024 01:48:33 -0500 Subject: [PATCH 087/112] Pokemon RB: Fix Incorrect Hidden Item Location in Seafoam Islands B2F (#4304) --- worlds/pokemon_rb/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 943204ceaf75..467139c39e94 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -401,7 +401,7 @@ def __init__(self, flag): LocationData("Cerulean Cave B1F-E", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24), inclusion=hidden_items), - LocationData("Seafoam Islands B2F-NW", "Hidden Item Rock", "Nugget", rom_addresses['Hidden_Item_Seafoam_Islands_B2F'], Hidden(25), inclusion=hidden_items), + LocationData("Seafoam Islands B2F-SW", "Hidden Item Rock", "Nugget", rom_addresses['Hidden_Item_Seafoam_Islands_B2F'], Hidden(25), inclusion=hidden_items), LocationData("Seafoam Islands B4F-W", "Hidden Item Corner Island", "Ultra Ball", rom_addresses['Hidden_Item_Seafoam_Islands_B4F'], Hidden(26), inclusion=hidden_items), LocationData("Pokemon Mansion 1F", "Hidden Item Block Near Entrance Carpet", "Moon Stone", rom_addresses['Hidden_Item_Pokemon_Mansion_1F'], Hidden(27), inclusion=hidden_moon_stones), LocationData("Pokemon Mansion 3F-SW", "Hidden Item Behind Burglar", "Max Revive", rom_addresses['Hidden_Item_Pokemon_Mansion_3F'], Hidden(28), inclusion=hidden_items), From 85a0d59f739a199bf4e72612d29183f86d00a66e Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Thu, 5 Dec 2024 04:23:26 -0500 Subject: [PATCH 088/112] LADX: text shuffle exclusions (#3919) * text shuffle exclusions Exclude owl statues, library books, goal sign, signpost maze, and various rupee prices from text shuffle * clearer variable name --- worlds/ladx/LADXR/generator.py | 44 +++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index b402b3d88919..504dfc78eced 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -340,11 +340,53 @@ def gen_hint(): patches.enemies.doubleTrouble(rom) if world.options.text_shuffle: + excluded_ids = [ + # Overworld owl statues + 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D, + + # Dungeon owls + 0x288, 0x280, # D1 + 0x28A, 0x289, 0x281, # D2 + 0x282, 0x28C, 0x28B, # D3 + 0x283, # D4 + 0x28D, 0x284, # D5 + 0x285, 0x28F, 0x28E, # D6 + 0x291, 0x290, 0x286, # D7 + 0x293, 0x287, 0x292, # D8 + 0x263, # D0 + + # Hint books + 0x267, # color dungeon + 0x200, 0x201, + 0x202, 0x203, + 0x204, 0x205, + 0x206, 0x207, + 0x208, 0x209, + 0x20A, 0x20B, + 0x20C, + 0x20D, 0x20E, + 0x217, 0x218, 0x219, 0x21A, + + # Goal sign + 0x1A3, + + # Signpost maze + 0x1A9, 0x1AA, 0x1AB, 0x1AC, 0x1AD, + + # Prices + 0x02C, 0x02D, 0x030, 0x031, 0x032, 0x033, # Shop items + 0x03B, # Trendy Game + 0x045, # Fisherman + 0x018, 0x019, # Crazy Tracy + 0x0DC, # Mamu + 0x0F0, # Raft ride + ] + excluded_texts = [ rom.texts[excluded_id] for excluded_id in excluded_ids] buckets = defaultdict(list) # For each ROM bank, shuffle text within the bank for n, data in enumerate(rom.texts._PointerTable__data): # Don't muck up which text boxes are questions and which are statements - if type(data) != int and data and data != b'\xFF': + if type(data) != int and data and data != b'\xFF' and data not in excluded_texts: buckets[(rom.texts._PointerTable__banks[n], data[len(data) - 1] == 0xfe)].append((n, data)) for bucket in buckets.values(): # For each bucket, make a copy and shuffle From d80069385dc6166c2333d2c2360d47861a138d87 Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Thu, 5 Dec 2024 06:03:16 -0500 Subject: [PATCH 089/112] LADX: tweak in-game hints (#3920) * dont show local player name in hint * add option to disable hints --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/ladx/LADXR/generator.py | 4 +++- worlds/ladx/Options.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 504dfc78eced..046b51815cba 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -266,6 +266,8 @@ def generateRom(args, world: "LinksAwakeningWorld"): our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification] def gen_hint(): + if not world.options.in_game_hints: + return 'Hints are disabled!' chance = world.random.uniform(0, 1) if chance < JUNK_HINT: return None @@ -286,7 +288,7 @@ def gen_hint(): else: location_name = location.name - hint = f"{name} {location.item} is at {location_name}" + hint = f"{name} {location.item.name} is at {location_name}" if location.player != world.player: # filter out { and } since they cause issues with string.format later on player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "") diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 863e80fd036b..9414a7e3c89b 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -498,6 +498,13 @@ class Warps(Choice): default = option_vanilla +class InGameHints(DefaultOnToggle): + """ + When enabled, owl statues and library books may indicate the location of your items in the multiworld. + """ + display_name = "In-game Hints" + + ladx_option_groups = [ OptionGroup("Goal Options", [ Goal, @@ -518,6 +525,7 @@ class Warps(Choice): TradeQuest, Rooster, TrendyGame, + InGameHints, NagMessages, BootsControls ]), @@ -574,6 +582,7 @@ class LinksAwakeningOptions(PerGameCommonOptions): nag_messages: NagMessages ap_title_screen: APTitleScreen boots_controls: BootsControls + in_game_hints: InGameHints warp_improvements: Removed additional_warp_points: Removed From 4d42814f5d6a7315bb177f76d1f690ff6454b9ef Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Thu, 5 Dec 2024 06:06:52 -0500 Subject: [PATCH 090/112] LADX: more item groups, location groups, keysanity preset (#3936) * add groups and a preset * formatting * typing * alias groups for progressive items * add bush breakers item group * fix typo * some manual location groups * drop dummy dungeon items from groups --- worlds/ladx/Items.py | 136 ++++++++++++++++++++++++++++++++++++++- worlds/ladx/Locations.py | 35 +++++++++- worlds/ladx/__init__.py | 24 ++++--- 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index b9e1eeab3e69..2a64c59394e6 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -26,7 +26,7 @@ class DungeonItemData(ItemData): @property def dungeon_index(self): return int(self.ladxr_id[-1]) - + @property def dungeon_item_type(self): s = self.ladxr_id[:-1] @@ -174,7 +174,7 @@ class ItemName: TRADING_ITEM_SCALE = "Scale" TRADING_ITEM_MAGNIFYING_GLASS = "Magnifying Glass" -trade_item_prog = ItemClassification.progression +trade_item_prog = ItemClassification.progression links_awakening_items = [ ItemData(ItemName.POWER_BRACELET, "POWER_BRACELET", ItemClassification.progression), @@ -303,3 +303,135 @@ class ItemName: links_awakening_items_by_name = { item.item_name : item for item in links_awakening_items } + +links_awakening_item_name_groups: typing.Dict[str, typing.Set[str]] = { + "Instruments": { + "Full Moon Cello", + "Conch Horn", + "Sea Lily's Bell", + "Surf Harp", + "Wind Marimba", + "Coral Triangle", + "Organ of Evening Calm", + "Thunder Drum", + }, + "Entrance Keys": { + "Tail Key", + "Angler Key", + "Face Key", + "Bird Key", + "Slime Key", + }, + "Nightmare Keys": { + "Nightmare Key (Angler's Tunnel)", + "Nightmare Key (Bottle Grotto)", + "Nightmare Key (Catfish's Maw)", + "Nightmare Key (Color Dungeon)", + "Nightmare Key (Eagle's Tower)", + "Nightmare Key (Face Shrine)", + "Nightmare Key (Key Cavern)", + "Nightmare Key (Tail Cave)", + "Nightmare Key (Turtle Rock)", + }, + "Small Keys": { + "Small Key (Angler's Tunnel)", + "Small Key (Bottle Grotto)", + "Small Key (Catfish's Maw)", + "Small Key (Color Dungeon)", + "Small Key (Eagle's Tower)", + "Small Key (Face Shrine)", + "Small Key (Key Cavern)", + "Small Key (Tail Cave)", + "Small Key (Turtle Rock)", + }, + "Compasses": { + "Compass (Angler's Tunnel)", + "Compass (Bottle Grotto)", + "Compass (Catfish's Maw)", + "Compass (Color Dungeon)", + "Compass (Eagle's Tower)", + "Compass (Face Shrine)", + "Compass (Key Cavern)", + "Compass (Tail Cave)", + "Compass (Turtle Rock)", + }, + "Maps": { + "Dungeon Map (Angler's Tunnel)", + "Dungeon Map (Bottle Grotto)", + "Dungeon Map (Catfish's Maw)", + "Dungeon Map (Color Dungeon)", + "Dungeon Map (Eagle's Tower)", + "Dungeon Map (Face Shrine)", + "Dungeon Map (Key Cavern)", + "Dungeon Map (Tail Cave)", + "Dungeon Map (Turtle Rock)", + }, + "Stone Beaks": { + "Stone Beak (Angler's Tunnel)", + "Stone Beak (Bottle Grotto)", + "Stone Beak (Catfish's Maw)", + "Stone Beak (Color Dungeon)", + "Stone Beak (Eagle's Tower)", + "Stone Beak (Face Shrine)", + "Stone Beak (Key Cavern)", + "Stone Beak (Tail Cave)", + "Stone Beak (Turtle Rock)", + }, + "Trading Items": { + "Yoshi Doll", + "Ribbon", + "Dog Food", + "Bananas", + "Stick", + "Honeycomb", + "Pineapple", + "Hibiscus", + "Letter", + "Broom", + "Fishing Hook", + "Necklace", + "Scale", + "Magnifying Glass", + }, + "Rupees": { + "20 Rupees", + "50 Rupees", + "100 Rupees", + "200 Rupees", + "500 Rupees", + }, + "Upgrades": { + "Max Powder Upgrade", + "Max Bombs Upgrade", + "Max Arrows Upgrade", + }, + "Songs": { + "Ballad of the Wind Fish", + "Manbo's Mambo", + "Frog's Song of Soul", + }, + "Tunics": { + "Red Tunic", + "Blue Tunic", + }, + "Bush Breakers": { + "Progressive Power Bracelet", + "Magic Rod", + "Magic Powder", + "Bomb", + "Progressive Sword", + "Boomerang", + }, + "Sword": { + "Progressive Sword", + }, + "Shield": { + "Progressive Shield", + }, + "Power Bracelet": { + "Progressive Power Bracelet", + }, + "Bracelet": { + "Progressive Power Bracelet", + }, +} diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index f29355f2ba86..8670738e0869 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -1,5 +1,5 @@ from BaseClasses import Region, Entrance, Location, CollectionState - +import typing from .LADXR.checkMetadata import checkMetadataTable from .Common import * @@ -25,6 +25,39 @@ def meta_to_name(meta): return f"{meta.name} ({meta.area})" +def get_location_name_groups() -> typing.Dict[str, typing.Set[str]]: + groups = { + "Instrument Pedestals": { + "Full Moon Cello (Tail Cave)", + "Conch Horn (Bottle Grotto)", + "Sea Lily's Bell (Key Cavern)", + "Surf Harp (Angler's Tunnel)", + "Wind Marimba (Catfish's Maw)", + "Coral Triangle (Face Shrine)", + "Organ of Evening Calm (Eagle's Tower)", + "Thunder Drum (Turtle Rock)", + }, + "Boss Rewards": { + "Moldorm Heart Container (Tail Cave)", + "Genie Heart Container (Bottle Grotto)", + "Slime Eye Heart Container (Key Cavern)", + "Angler Fish Heart Container (Angler's Tunnel)", + "Slime Eel Heart Container (Catfish's Maw)", + "Facade Heart Container (Face Shrine)", + "Evil Eagle Heart Container (Eagle's Tower)", + "Hot Head Heart Container (Turtle Rock)", + "Tunic Fairy Item 1 (Color Dungeon)", + "Tunic Fairy Item 2 (Color Dungeon)", + }, + } + # Add region groups + for s, v in checkMetadataTable.items(): + if s == "None": + continue + groups.setdefault(v.area, set()).add(meta_to_name(v)) + return groups + +links_awakening_location_name_groups = get_location_name_groups() def get_locations_to_id(): ret = { diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 2846b40e67d9..7499aca8c404 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -13,7 +13,8 @@ from worlds.AutoWorld import WebWorld, World from .Common import * from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData, - ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name) + ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name, + links_awakening_item_name_groups) from .LADXR import generator from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.locations.constants import CHEST_ITEMS @@ -23,7 +24,8 @@ from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, - create_regions_from_ladxr, get_locations_to_id) + create_regions_from_ladxr, get_locations_to_id, + links_awakening_location_name_groups) from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups from .Rom import LADXDeltaPatch, get_base_rom_path @@ -66,6 +68,15 @@ class LinksAwakeningWebWorld(WebWorld): )] theme = "dirt" option_groups = ladx_option_groups + options_presets: typing.Dict[str, typing.Dict[str, typing.Any]] = { + "Keysanity": { + "shuffle_nightmare_keys": "any_world", + "shuffle_small_keys": "any_world", + "shuffle_maps": "any_world", + "shuffle_compasses": "any_world", + "shuffle_stone_beaks": "any_world", + } + } class LinksAwakeningWorld(World): """ @@ -98,12 +109,9 @@ class LinksAwakeningWorld(World): # Items can be grouped using their names to allow easy checking if any item # from that group has been collected. Group names can also be used for !hint - item_name_groups = { - "Instruments": { - "Full Moon Cello", "Conch Horn", "Sea Lily's Bell", "Surf Harp", - "Wind Marimba", "Coral Triangle", "Organ of Evening Calm", "Thunder Drum" - }, - } + item_name_groups = links_awakening_item_name_groups + + location_name_groups = links_awakening_location_name_groups prefill_dungeon_items = None From 203d89d1d38102800bfc0aa4c5338f7fc3113dec Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Thu, 5 Dec 2024 10:32:45 -0500 Subject: [PATCH 091/112] LADX: upstream logic updates (#3963) * Fully updates requirements.py to live LADXR (#19) * Updates dungeon2.py to LADXR-Live (#20) No logic changes or bugfix are in this file. It is only code cleanup. * Update dungeon1.py (#21) - The Three of a Kind with Bomb is moved from Normal to Hard Logic The rest is code cleanup. lines 22-25 | 22-26 & 33 | 34 remain different in AP | Upstream with no effective difference * Fully updates dungeon3.py to LADXR-live (#22) Logic Changes: - Hard mode now considers killing the enemies in the top room with pot Everything else is cleanup. * Fully update dungeon4.py to LADXR-live logic (#23) Logic Changes: - Hard Logic: Removes Feather requirement from grabbing the Pit Key - Hell logic: new hookshot clip (line 64) - Hell logic: hookshot spam over the first pit of crossroads, then buffer down (line 69) - Hell logic: push block left of keyblock up, then shaq jump off the left wall and pause buffer to land on keyblock. - Hell logic: split zol for more entities, and clip through the block left of keyblock by hookshot spam The rest is code cleanup * Updates dungeon5.py mostly to LADXR-Live Logic (#24) Logic Changes: - Hell logic: use zoomerang dashing left to get an unclipped boots superjump off the right wall over the block. reverse is push block (line 69) The rest is cleanup. The upstream splits the post_gohma region into pre_gohma, gohma and post_gohma. I did not implement this yet as I do not know the implications. To port this the following lines need to be changed (AP | LADXR): 18 | 18-20; 55 | 58; 65 | 68-69 * Fully update dungeon6.py logic (#25) Logic Changes: - Hard logic: allow damage boosting past the mini thwomps - Glitched logic: bomb triggering elephants in two cases Everything else is cleanup * Fully update dungeon7.py to LADXR-live logic (#26) Logic Changes: - Hard logic: Three of a Kind is now possible with bombs only Everything else is code cleanup * Fully updates dungeon8.py to LADXR-live (#27) Logic change: - Hard logic: allows to drop the Gibdos into holes as a way to kill them - Glitched logic: underground section with fire balls jumping up out of lava. Use boots superjump off left wall to jump over the pot blocking the way The rest is code cleanup * Fully update dungeonColor.py to LADXR-live (#28) Logic changes: - Normal logic: Karakoros now need power bracelet to put them into their holes - Hard logic: Karakoros without power bracelet but with weapon - Hell logic: Karakoros with only bombs Everything else is code cleanup * Updating overworld.py (#29) * Updating overworld.py This tries to update all logic of the Overworld. Logic changes include: - Normal logic: requires hookshot or shield to traverse Armos Cave - Hard logic: Traverse Armos Cave with nothing (formerly normal logic) - Hard logic: get the animal village bomb cave check with jump and boomerang - Hard logic: use rooster to go to D7 - Lots of Jesus Rooster Jumps I stopped counting and need to go over this again. Also, please investigate line 474 AP because it's removed in LADXR-Upstream and I don't know why. * remove featherless fisher under bridge from hard it was moved to hell upstream and its already present in our code --------- Co-authored-by: Alex Nordstrom * fixes * add test messages * Adds Pegasus Boots to the test (#31) * Fix d6 boss_key logic (#30) * restore hardmode logic * higher logic fixes * add bush requirement to the raft in case the player needs to farm rupees to play again --------- Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com> --- worlds/ladx/LADXR/logic/dungeon1.py | 15 +- worlds/ladx/LADXR/logic/dungeon2.py | 14 +- worlds/ladx/LADXR/logic/dungeon3.py | 38 ++-- worlds/ladx/LADXR/logic/dungeon4.py | 44 ++-- worlds/ladx/LADXR/logic/dungeon5.py | 49 ++--- worlds/ladx/LADXR/logic/dungeon6.py | 36 ++-- worlds/ladx/LADXR/logic/dungeon7.py | 35 +-- worlds/ladx/LADXR/logic/dungeon8.py | 50 +++-- worlds/ladx/LADXR/logic/dungeonColor.py | 16 +- worlds/ladx/LADXR/logic/overworld.py | 274 ++++++++++++++---------- worlds/ladx/LADXR/logic/requirements.py | 72 ++++++- worlds/ladx/test/TestDungeonLogic.py | 26 +-- 12 files changed, 399 insertions(+), 270 deletions(-) diff --git a/worlds/ladx/LADXR/logic/dungeon1.py b/worlds/ladx/LADXR/logic/dungeon1.py index 82321a1c0d65..645c50d1d5e5 100644 --- a/worlds/ladx/LADXR/logic/dungeon1.py +++ b/worlds/ladx/LADXR/logic/dungeon1.py @@ -9,7 +9,7 @@ def __init__(self, options, world_setup, r): entrance.add(DungeonChest(0x113), DungeonChest(0x115), DungeonChest(0x10E)) Location(dungeon=1).add(DroppedKey(0x116)).connect(entrance, OR(BOMB, r.push_hardhat)) # hardhat beetles (can kill with bomb) Location(dungeon=1).add(DungeonChest(0x10D)).connect(entrance, OR(r.attack_hookshot_powder, SHIELD)) # moldorm spawn chest - stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, r.attack_hookshot) # 2 stalfos 2 keese room + stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, AND(OR(r.attack_skeleton, SHIELD),r.attack_hookshot_powder)) # 2 stalfos 2 keese room Location(dungeon=1).add(DungeonChest(0x10C)).connect(entrance, BOMB) # hidden seashell room dungeon1_upper_left = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) if options.owlstatues == "both" or options.owlstatues == "dungeon": @@ -19,21 +19,22 @@ def __init__(self, options, world_setup, r): dungeon1_right_side = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1) - Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot, SHIELD)) # three of a kind, shield stops the suit from changing + dungeon1_3_of_a_kind = Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot_no_bomb, SHIELD)) # three of a kind, shield stops the suit from changing dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER)) dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1) - Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) + boss = Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) - if options.logic not in ('normal', 'casual'): + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': stalfos_keese_room.connect(entrance, r.attack_hookshot_powder) # stalfos jump away when you press a button. - + dungeon1_3_of_a_kind.connect(dungeon1_right_side, BOMB) # use timed bombs to match the 3 of a kinds + if options.logic == 'glitched' or options.logic == 'hell': - boss_key.connect(entrance, FEATHER) # super jump + boss_key.connect(entrance, r.super_jump_feather) # super jump dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom if options.logic == 'hell': feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall - boss_key.connect(entrance, FOUND(KEY1,3)) # damage boost off the hardhat to cross the pit + boss_key.connect(entrance, AND(r.damage_boost, FOUND(KEY1,3))) # damage boost off the hardhat to cross the pit self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon2.py b/worlds/ladx/LADXR/logic/dungeon2.py index 3bb95edbc8bd..6ee6cc4a8020 100644 --- a/worlds/ladx/LADXR/logic/dungeon2.py +++ b/worlds/ladx/LADXR/logic/dungeon2.py @@ -14,7 +14,7 @@ def __init__(self, options, world_setup, r): Location(dungeon=2).add(DungeonChest(0x137)).connect(dungeon2_r2, AND(KEY2, FOUND(KEY2, 5), OR(r.rear_attack, r.rear_attack_range))) # compass chest if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=2).add(OwlStatue(0x133)).connect(dungeon2_r2, STONE_BEAK2) - dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.attack_hookshot) # first chest with key, can hookshot the switch in previous room + dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.hit_switch) # first chest with key, can hookshot the switch in previous room dungeon2_r4 = Location(dungeon=2).add(DungeonChest(0x139)).connect(dungeon2_r3, FEATHER) # button spawn chest if options.logic == "casual": shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, AND(FEATHER, OR(r.rear_attack, r.rear_attack_range))) # shyguy drop key @@ -39,16 +39,16 @@ def __init__(self, options, world_setup, r): if options.logic == 'glitched' or options.logic == 'hell': dungeon2_ghosts_chest.connect(dungeon2_ghosts_room, SWORD) # use sword to spawn ghosts on other side of the room so they run away (logically irrelevant because of torches at start) - dungeon2_r6.connect(miniboss, FEATHER) # superjump to staircase next to hinox. + dungeon2_r6.connect(miniboss, r.super_jump_feather) # superjump to staircase next to hinox. if options.logic == 'hell': - dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, PEGASUS_BOOTS)) # use boots to jump over the pits - dungeon2_r4.connect(dungeon2_r3, OR(PEGASUS_BOOTS, HOOKSHOT)) # can use both pegasus boots bonks or hookshot spam to cross the pit room + dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, r.boots_bonk_pit)) # use boots to jump over the pits + dungeon2_r4.connect(dungeon2_r3, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # can use both pegasus boots bonks or hookshot spam to cross the pit room dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4 - miniboss.connect(dungeon2_r5, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section + miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice - dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, AND(PEGASUS_BOOTS, FEATHER))) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) - dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically + dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, r.boots_jump)) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) + dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon3.py b/worlds/ladx/LADXR/logic/dungeon3.py index e65c7da0bafc..33782be16c87 100644 --- a/worlds/ladx/LADXR/logic/dungeon3.py +++ b/worlds/ladx/LADXR/logic/dungeon3.py @@ -20,8 +20,8 @@ def __init__(self, options, world_setup, r): Location(dungeon=3).add(OwlStatue(0x154)).connect(area_up, STONE_BEAK3) dungeon3_raised_blocks_north = Location(dungeon=3).add(DungeonChest(0x14C)) # chest locked behind raised blocks near staircase dungeon3_raised_blocks_east = Location(dungeon=3).add(DungeonChest(0x150)) # chest locked behind raised blocks next to slime chest - area_up.connect(dungeon3_raised_blocks_north, r.attack_hookshot, one_way=True) # hit switch to reach north chest - area_up.connect(dungeon3_raised_blocks_east, r.attack_hookshot, one_way=True) # hit switch to reach east chest + area_up.connect(dungeon3_raised_blocks_north, r.hit_switch, one_way=True) # hit switch to reach north chest + area_up.connect(dungeon3_raised_blocks_east, r.hit_switch, one_way=True) # hit switch to reach east chest area_left = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) area_left_key_drop = Location(dungeon=3).add(DroppedKey(0x155)).connect(area_left, r.attack_hookshot) # west key drop (no longer requires feather to get across hole), can use boomerang to knock owls into pit @@ -54,28 +54,30 @@ def __init__(self, options, world_setup, r): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': dungeon3_3_bombite_room.connect(area_right, BOOMERANG) # 3 bombite room from the left side, grab item with boomerang - dungeon3_reverse_eye.connect(entrance, HOOKSHOT) # hookshot the chest to get to the right side - dungeon3_north_key_drop.connect(area_up, POWER_BRACELET) # use pots to kill the enemies - dungeon3_south_key_drop.connect(area_down, POWER_BRACELET) # use pots to kill enemies + dungeon3_reverse_eye.connect(entrance, r.hookshot_over_pit) # hookshot the chest to get to the right side + dungeon3_north_key_drop.connect(area_up, r.throw_pot) # use pots to kill the enemies + dungeon3_south_key_drop.connect(area_down, r.throw_pot) # use pots to kill enemies + area_up.connect(dungeon3_raised_blocks_north, r.throw_pot, one_way=True) # use pots to hit the switch + area_up.connect(dungeon3_raised_blocks_east, AND(r.throw_pot, r.attack_hookshot_powder), one_way=True) # use pots to hit the switch if options.logic == 'glitched' or options.logic == 'hell': - area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, FEATHER), one_way=True) # use superjump to get over the bottom left block - area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, HOOKSHOT), FEATHER), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block - area3.connect(dungeon3_zol_stalfos, HOOKSHOT, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol - dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap - dungeon3_post_dodongo_chest.connect(area_right, AND(FEATHER, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key + area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, r.super_jump_feather), one_way=True) # use superjump to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, r.hookshot_clip_block), r.shaq_jump), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block + area3.connect(dungeon3_zol_stalfos, r.hookshot_clip_block, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol + dungeon3_nightmare_key_chest.connect(area_right, AND(r.super_jump_feather, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap + dungeon3_post_dodongo_chest.connect(area_right, AND(r.super_jump_feather, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key if options.logic == 'hell': - area2.connect(dungeon3_raised_blocks_east, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop to get over the bottom left block - area3.connect(dungeon3_raised_blocks_north, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop off top wall or left wall to get on raised blocks - area_up.connect(dungeon3_zol_stalfos, AND(FEATHER, OR(BOW, MAGIC_ROD, SWORD)), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles - area_left_key_drop.connect(area_left, SHIELD) # knock everything into the pit including the teleporting owls - dungeon3_south_key_drop.connect(area_down, SHIELD) # knock everything into the pit including the teleporting owls - dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, SHIELD)) # superjump into jumping stalfos and shield bump to right ledge - dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest + area2.connect(dungeon3_raised_blocks_east, r.boots_superhop, one_way=True) # use boots superhop to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, r.boots_superhop, one_way=True) # use boots superhop off top wall or left wall to get on raised blocks + area_up.connect(dungeon3_zol_stalfos, AND(r.super_jump_feather, r.attack_skeleton), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles + area_left_key_drop.connect(area_left, r.shield_bump) # knock everything into the pit including the teleporting owls + dungeon3_south_key_drop.connect(area_down, r.shield_bump) # knock everything into the pit including the teleporting owls + dungeon3_nightmare_key_chest.connect(area_right, AND(r.super_jump_feather, r.shield_bump)) # superjump into jumping stalfos and shield bump to right ledge + dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, r.pit_buffer_boots, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest compass_chest.connect(dungeon3_3_bombite_room, OR(BOW, MAGIC_ROD, AND(OR(FEATHER, PEGASUS_BOOTS), OR(SWORD, MAGIC_POWDER))), one_way=True) # 3 bombite room from the left side, use a bombite to blow open the wall without bombs pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, POWER_BRACELET)) # use bracelet super bounce glitch to pass through first part underground section - pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, PEGASUS_BOOTS, "MEDICINE2")) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase + pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, r.boots_bonk_2d_spikepit)) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon4.py b/worlds/ladx/LADXR/logic/dungeon4.py index 7d71c89f0c86..a7e06557fa12 100644 --- a/worlds/ladx/LADXR/logic/dungeon4.py +++ b/worlds/ladx/LADXR/logic/dungeon4.py @@ -42,32 +42,36 @@ def __init__(self, options, world_setup, r): boss = Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(before_boss, AND(NIGHTMARE_KEY4, r.boss_requirements[world_setup.boss_mapping[3]])) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': - sidescroller_key.connect(before_miniboss, AND(FEATHER, BOOMERANG)) # grab the key jumping over the water and boomerang downwards - sidescroller_key.connect(before_miniboss, AND(POWER_BRACELET, FLIPPERS)) # kill the zols with the pots in the room to spawn the key - rightside_crossroads.connect(entrance, FEATHER) # jump across the corners - puddle_crack_block_chest.connect(rightside_crossroads, FEATHER) # jump around the bombable block - north_crossroads.connect(entrance, FEATHER) # jump across the corners - after_double_lock.connect(entrance, FEATHER) # jump across the corners - dungeon4_puddle_before_crossroads.connect(after_double_lock, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers - center_puddle_chest.connect(before_miniboss, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers + sidescroller_key.connect(before_miniboss, BOOMERANG) # fall off the bridge and boomerang downwards before hitting the water to grab the item + sidescroller_key.connect(before_miniboss, AND(r.throw_pot, FLIPPERS)) # kill the zols with the pots in the room to spawn the key + rightside_crossroads.connect(entrance, r.tight_jump) # jump across the corners + puddle_crack_block_chest.connect(rightside_crossroads, r.tight_jump) # jump around the bombable block + north_crossroads.connect(entrance, r.tight_jump) # jump across the corners + after_double_lock.connect(entrance, r.tight_jump) # jump across the corners + dungeon4_puddle_before_crossroads.connect(after_double_lock, r.tight_jump) # With a tight jump feather is enough to cross the puddle without flippers + center_puddle_chest.connect(before_miniboss, r.tight_jump) # With a tight jump feather is enough to cross the puddle without flippers miniboss = Location(dungeon=4).connect(terrace_zols_chest, None, one_way=True) # reach flippers chest through the miniboss room without pulling the lever - to_the_nightmare_key.connect(left_water_area, FEATHER) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section - before_boss.connect(left_water_area, FEATHER) # jump to the bottom right corner of boss door room + to_the_nightmare_key.connect(left_water_area, r.tight_jump) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section + before_boss.connect(left_water_area, r.tight_jump) # jump to the bottom right corner of boss door room if options.logic == 'glitched' or options.logic == 'hell': - pushable_block_chest.connect(rightside_crossroads, FLIPPERS) # sideways block push to skip bombs - sidescroller_key.connect(before_miniboss, AND(FEATHER, OR(r.attack_hookshot_powder, POWER_BRACELET))) # superjump into the hole to grab the key while falling into the water - miniboss.connect(before_miniboss, FEATHER) # use jesus jump to transition over the water left of miniboss + pushable_block_chest.connect(rightside_crossroads, AND(r.sideways_block_push, FLIPPERS)) # sideways block push to skip bombs + sidescroller_key.connect(before_miniboss, AND(r.super_jump_feather, OR(r.attack_hookshot_powder, r.throw_pot))) # superjump into the hole to grab the key while falling into the water + miniboss.connect(before_miniboss, r.jesus_jump) # use jesus jump to transition over the water left of miniboss if options.logic == 'hell': - rightside_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit - pushable_block_chest.connect(rightside_crossroads, OR(PEGASUS_BOOTS, FEATHER)) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest - after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), PEGASUS_BOOTS), one_way=True) # use boots bonks to cross the water gaps + rightside_crossroads.connect(entrance, AND(r.pit_buffer_boots, r.hookshot_spam_pit)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit + rightside_crossroads.connect(after_double_lock, AND(OR(BOMB, BOW), r.hookshot_clip_block)) # split zols for more entities, and clip through the block against the right wall + pushable_block_chest.connect(rightside_crossroads, AND(r.sideways_block_push, OR(r.jesus_buffer, r.jesus_jump))) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest + after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), r.pit_buffer_boots), one_way=True) # use boots bonks to cross the water gaps + after_double_lock.connect(entrance, r.pit_buffer_boots) # boots bonk + pit buffer to the bottom + after_double_lock.connect(entrance, AND(r.pit_buffer, r.hookshot_spam_pit)) # hookshot spam over the first pit of crossroads, then buffer down + dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up north_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into wall of the first pit, then boots bonk towards the top and hookshot spam to get across (easier with Piece of Power) - after_double_lock.connect(entrance, PEGASUS_BOOTS) # boots bonk + pit buffer to the bottom - dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up - to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, PEGASUS_BOOTS)) # Use flippers for puzzle and boots bonk to get through 2d section - before_boss.connect(left_water_area, PEGASUS_BOOTS) # boots bonk across bottom wall then boots bonk to the platform before boss door + before_miniboss.connect(north_crossroads, AND(r.shaq_jump, r.hookshot_clip_block)) # push block left of keyblock up, then shaq jump off the left wall and pause buffer to land on keyblock. + before_miniboss.connect(north_crossroads, AND(OR(BOMB, BOW), r.hookshot_clip_block)) # split zol for more entities, and clip through the block left of keyblock by hookshot spam + to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, r.boots_bonk)) # use flippers for puzzle and boots bonk to get through 2d section + before_boss.connect(left_water_area, r.pit_buffer_boots) # boots bonk across bottom wall then boots bonk to the platform before boss door self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon5.py b/worlds/ladx/LADXR/logic/dungeon5.py index b8e013066c50..b61e48e255d0 100644 --- a/worlds/ladx/LADXR/logic/dungeon5.py +++ b/worlds/ladx/LADXR/logic/dungeon5.py @@ -39,43 +39,44 @@ def __init__(self, options, world_setup, r): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': blade_trap_chest.connect(area2, AND(FEATHER, r.attack_hookshot_powder)) # jump past the blade traps - boss_key.connect(after_stalfos, AND(FLIPPERS, FEATHER, PEGASUS_BOOTS)) # boots jump across + boss_key.connect(after_stalfos, AND(FLIPPERS, r.boots_jump)) # boots jump across after_stalfos.connect(after_keyblock_boss, AND(FEATHER, r.attack_hookshot_powder)) # circumvent stalfos by going past gohma and backwards from boss door if butterfly_owl: - butterfly_owl.connect(after_stalfos, AND(PEGASUS_BOOTS, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge - after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block - staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk in 2d section to skip feather - north_of_crossroads.connect(after_stalfos, HOOKSHOT) # hookshot to the right block to cross pits - first_bridge_chest.connect(north_of_crossroads, FEATHER) # tight jump from bottom wall clipped to make it over the pits + butterfly_owl.connect(after_stalfos, AND(r.boots_bonk, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge + after_stalfos.connect(staircase_before_boss, AND(r.boots_bonk, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block + staircase_before_boss.connect(post_gohma, AND(r.boots_bonk, HOOKSHOT)) # boots bonk in 2d section to skip feather + north_of_crossroads.connect(after_stalfos, r.hookshot_over_pit) # hookshot to the right block to cross pits + first_bridge_chest.connect(north_of_crossroads, AND(r.wall_clip, r.tight_jump)) # tight jump from bottom wall clipped to make it over the pits after_keyblock_boss.connect(after_stalfos, AND(FEATHER, r.attack_hookshot_powder)) # jump from bottom left to top right, skipping the keyblock - before_boss.connect(after_stalfos, AND(FEATHER, PEGASUS_BOOTS, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump + before_boss.connect(after_stalfos, AND(r.boots_jump, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump if options.logic == 'glitched' or options.logic == 'hell': - start_hookshot_chest.connect(entrance, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + start_hookshot_chest.connect(entrance, r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across the pits post_gohma.connect(area2, HOOKSHOT) # glitch through the blocks/pots with hookshot. Zoomerang can be used but has no logical implications because of 2d section requiring hookshot - north_bridge_chest.connect(north_of_crossroads, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits - east_bridge_chest.connect(first_bridge_chest, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits - #after_stalfos.connect(staircase_before_boss, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block TODO: nagmessages - after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall + north_bridge_chest.connect(north_of_crossroads, r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across the pits + east_bridge_chest.connect(first_bridge_chest, r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across the pits + #after_stalfos.connect(staircase_before_boss, AND(r.text_clip, r.super_jump)) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block + after_stalfos.connect(staircase_before_boss, r.super_jump_boots) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall if options.logic == 'hell': - start_hookshot_chest.connect(entrance, PEGASUS_BOOTS) # use pit buffer to clip into the bottom wall and boots bonk off the wall again - fourth_stalfos_area.connect(compass, AND(PEGASUS_BOOTS, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section - blade_trap_chest.connect(area2, AND(PEGASUS_BOOTS, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps + start_hookshot_chest.connect(entrance, r.pit_buffer_boots) # use pit buffer to clip into the bottom wall and boots bonk off the wall again + fourth_stalfos_area.connect(compass, AND(r.boots_bonk_2d_hell, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section + blade_trap_chest.connect(area2, AND(r.pit_buffer_boots, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps post_gohma.connect(area2, AND(PEGASUS_BOOTS, FEATHER, POWER_BRACELET, r.attack_hookshot_powder)) # use boots jump in room with 2 zols + flying arrows to pit buffer above pot, then jump across. Sideways block push + pick up pots to reach post_gohma - staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, FEATHER)) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall - after_stalfos.connect(staircase_before_boss, AND(FEATHER, SWORD)) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block + staircase_before_boss.connect(post_gohma, r.boots_jump) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall + after_stalfos.connect(staircase_before_boss, r.super_jump_sword) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block after_stalfos.connect(area2, SWORD) # knock master stalfos down 255 times (about 23 minutes) - north_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering - first_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # get to first chest via the north chest with pit buffering - east_bridge_chest.connect(first_bridge_chest, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering + after_stalfos.connect(staircase_before_boss, r.zoomerang) # use zoomerang dashing left to get an unclipped boots superjump off the right wall over the block. reverse is push block + north_bridge_chest.connect(north_of_crossroads, r.boots_bonk_pit) # boots bonk across the pits with pit buffering + first_bridge_chest.connect(north_of_crossroads, r.boots_bonk_pit) # get to first chest via the north chest with pit buffering + east_bridge_chest.connect(first_bridge_chest, r.boots_bonk_pit) # boots bonk across the pits with pit buffering third_arena.connect(north_of_crossroads, SWORD) # can beat 3rd m.stalfos with 255 sword spins m_stalfos_drop.connect(third_arena, AND(FEATHER, SWORD)) # beat master stalfos by knocking it down 255 times x 4 (takes about 1.5h total) - m_stalfos_drop.connect(third_arena, AND(PEGASUS_BOOTS, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword - boss_key.connect(after_stalfos, FLIPPERS) # pit buffer across + m_stalfos_drop.connect(third_arena, AND(r.boots_bonk_2d_hell, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword + boss_key.connect(after_stalfos, AND(r.pit_buffer_itemless, FLIPPERS)) # pit buffer across if butterfly_owl: - after_keyblock_boss.connect(butterfly_owl, STONE_BEAK5, one_way=True) # pit buffer from top right to bottom in right pits room - before_boss.connect(after_stalfos, AND(FEATHER, SWORD)) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across + after_keyblock_boss.connect(butterfly_owl, AND(r.pit_buffer_itemless, STONE_BEAK5), one_way=True) # pit buffer from top right to bottom in right pits room + before_boss.connect(after_stalfos, r.super_jump_sword) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon6.py b/worlds/ladx/LADXR/logic/dungeon6.py index d67138b334a6..cde40a6b2df4 100644 --- a/worlds/ladx/LADXR/logic/dungeon6.py +++ b/worlds/ladx/LADXR/logic/dungeon6.py @@ -6,8 +6,8 @@ class Dungeon6: def __init__(self, options, world_setup, r, *, raft_game_chest=True): entrance = Location(dungeon=6) - Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(BOMB, BOW, MAGIC_ROD, COUNT(POWER_BRACELET, 2))) # 50 rupees - Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start + Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # 50 rupees + elephants_heart_chest = Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=6).add(OwlStatue(0x1BB)).connect(entrance, STONE_BEAK6) @@ -15,9 +15,9 @@ def __init__(self, options, world_setup, r, *, raft_game_chest=True): bracelet_chest = Location(dungeon=6).add(DungeonChest(0x1CE)).connect(entrance, AND(BOMB, FEATHER)) # left side - Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOW, MAGIC_ROD))) # 3 wizrobes raised blocks dont need to hit the switch + Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, r.attack_wizrobe)) # 3 wizrobes raised blocks don't need to hit the switch left_side = Location(dungeon=6).add(DungeonChest(0x1B9)).add(DungeonChest(0x1B3)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOOMERANG))) - Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(BOMB, BOW, MAGIC_ROD)) # 2 wizrobe drop key + Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(r.attack_wizrobe, BOW)) # 2 wizrobe drop key, allow bow as only 2 top_left = Location(dungeon=6).add(DungeonChest(0x1B0)).connect(left_side, COUNT(POWER_BRACELET, 2)) # top left chest horseheads if raft_game_chest: Location().add(Chest(0x06C)).connect(top_left, POWER_BRACELET) # seashell chest in raft game @@ -25,14 +25,15 @@ def __init__(self, options, world_setup, r, *, raft_game_chest=True): # right side to_miniboss = Location(dungeon=6).connect(entrance, KEY6) miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]])) - lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(OR(BOMB, BOW, MAGIC_ROD), COUNT(POWER_BRACELET, 2))) # waterway key + lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # waterway key medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine if options.owlstatues == "both" or options.owlstatues == "dungeon": lower_right_owl = Location(dungeon=6).add(OwlStatue(0x1D7)).connect(lower_right_side, AND(POWER_BRACELET, STONE_BEAK6)) center_1 = Location(dungeon=6).add(DroppedKey(0x1C3)).connect(miniboss, AND(COUNT(POWER_BRACELET, 2), FEATHER)) # tile room key drop - center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, AND(KEY6, FOUND(KEY6, 2))) # top right chest horseheads + center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS, r.attack_pols_voice, KEY6, FOUND(KEY6, 2))) # top right chest horseheads boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(AND(KEY6, FOUND(KEY6, 3), HOOKSHOT))) + center_2_and_upper_right_side.connect(boss_key, AND(HOOKSHOT, POWER_BRACELET, KEY6, FOUND(KEY6, 3)), one_way=True) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=6).add(OwlStatue(0x1B6)).connect(boss_key, STONE_BEAK6) @@ -40,19 +41,22 @@ def __init__(self, options, world_setup, r, *, raft_game_chest=True): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': bracelet_chest.connect(entrance, BOMB) # get through 2d section by "fake" jumping to the ladders - center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS)) # use a boots dash to get over the platforms - + center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), r.boots_dash_2d)) # use a boots dash to get over the platforms + center_2_and_upper_right_side.connect(center_1, AND(COUNT(POWER_BRACELET, 2), r.damage_boost, r.attack_pols_voice, FOUND(KEY6, 2))) # damage_boost past the mini_thwomps + if options.logic == 'glitched' or options.logic == 'hell': - entrance.connect(left_side, AND(POWER_BRACELET, FEATHER), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks - lower_right_side.connect(center_2_and_upper_right_side, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)), one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added - center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, FEATHER), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room - boss_key.connect(lower_right_side, FEATHER) # superjump from waterway to the left. POWER_BRACELET is implied from lower_right_side + elephants_heart_chest.connect(entrance, BOMB) # kill moldorm on screen above wizrobes, then bomb trigger on the right side to break elephant statue to get to the second chest + entrance.connect(left_side, AND(POWER_BRACELET, r.super_jump_feather), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks + lower_right_side.connect(center_2_and_upper_right_side, r.super_jump, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added + center_1.connect(miniboss, AND(r.bomb_trigger, OR(r.boots_dash_2d, FEATHER))) # bomb trigger the elephant statue after the miniboss + center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, r.shaq_jump), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room + boss_key.connect(lower_right_side, AND(POWER_BRACELET, r.super_jump_feather)) # superjump from waterway to the left. if options.logic == 'hell': - entrance.connect(left_side, AND(POWER_BRACELET, PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room - medicine_chest.connect(lower_right_side, AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW))) # can boots superhop off the top wall with bow or magic rod - center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough) - lower_right_side.connect(center_2_and_upper_right_side, FEATHER, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance + entrance.connect(left_side, AND(POWER_BRACELET, r.boots_superhop), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room + medicine_chest.connect(lower_right_side, r.boots_superhop) # can boots superhop off the top wall with bow or magic rod + center_1.connect(miniboss, AND(r.damage_boost_special, OR(r.bomb_trigger, COUNT(POWER_BRACELET, 2)))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough) + lower_right_side.connect(center_2_and_upper_right_side, r.super_jump_feather, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon7.py b/worlds/ladx/LADXR/logic/dungeon7.py index 594b4d083ca7..6188138f38ef 100644 --- a/worlds/ladx/LADXR/logic/dungeon7.py +++ b/worlds/ladx/LADXR/logic/dungeon7.py @@ -14,8 +14,8 @@ def __init__(self, options, world_setup, r): if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=7).add(OwlStatue(0x204)).connect(topright_pillar_area, STONE_BEAK7) topright_pillar_area.add(DungeonChest(0x209)) # stone slab chest can be reached by dropping down a hole - three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(r.attack_hookshot, AND(FEATHER, SHIELD))) # compass chest; path without feather with hitting switch by falling on the raised blocks. No bracelet because ball does not reset - bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.attack_hookshot) # area with hinox, be able to hit a switch to reach that area + three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(AND(r.hit_switch, r.attack_hookshot_no_bomb), AND(OR(BOMB, FEATHER), SHIELD))) # compass chest; either hit the switch, or have feather to fall on top of raised blocks. No bracelet because ball does not reset + bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.hit_switch) # area with hinox, be able to hit a switch to reach that area topleftF1_chest = Location(dungeon=7).add(DungeonChest(0x201)) # top left chest on F1 bottomleftF2_area.connect(topleftF1_chest, None, one_way = True) # drop down in left most holes of hinox room or tile room Location(dungeon=7).add(DroppedKey(0x21B)).connect(bottomleftF2_area, r.attack_hookshot) # hinox drop key @@ -23,9 +23,9 @@ def __init__(self, options, world_setup, r): if options.owlstatues == "both" or options.owlstatues == "dungeon": bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7)) nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss - mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.attack_hookshot) # mirror shield chest, need to be able to hit a switch to reach or + mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.hit_switch) # mirror shield chest, need to be able to hit a switch to reach or bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock - toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.attack_hookshot) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up + toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.hit_switch) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up final_pillar_area = Location(dungeon=7).add(DungeonChest(0x21C)).connect(bottomleftF2_area, AND(BOMB, HOOKSHOT)) # chest that needs to spawn to get to the last pillar final_pillar = Location(dungeon=7).connect(final_pillar_area, POWER_BRACELET) # decouple chest from pillar @@ -33,25 +33,28 @@ def __init__(self, options, world_setup, r): beamos_horseheads = Location(dungeon=7).add(DungeonChest(0x220)).connect(beamos_horseheads_area, POWER_BRACELET) # 100 rupee chest / medicine chest (DX) behind boss door pre_boss = Location(dungeon=7).connect(beamos_horseheads_area, HOOKSHOT) # raised plateau before boss staircase boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(pre_boss, r.boss_requirements[world_setup.boss_mapping[6]]) - + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + three_of_a_kind_north.connect(topright_pillar_area, BOMB) # use timed bombs to match the 3 of a kinds (south 3 of a kind room is implicite as normal logic can not reach chest without hookshot) + if options.logic == 'glitched' or options.logic == 'hell': - topright_pillar_area.connect(entrance, AND(FEATHER, SWORD)) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added - toprightF1_chest.connect(topright_pillar_area, FEATHER) # superjump from F1 switch room - topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, FEATHER) # superjump in top left pillar room over the blocks from right to left, to reach tile room + topright_pillar_area.connect(entrance, r.super_jump_sword) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added + toprightF1_chest.connect(topright_pillar_area, r.super_jump_feather) # superjump from F1 switch room + topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.super_jump_feather) # superjump in top left pillar room over the blocks from right to left, to reach tile room topleftF2_area.connect(topleftF1_chest, None, one_way = True) # fall down tile room holes on left side to reach top left chest on ground floor - topleftF1_chest.connect(bottomleftF2_area, AND(PEGASUS_BOOTS, FEATHER), one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area - final_pillar_area.connect(bottomleftF2_area, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path + topleftF1_chest.connect(bottomleftF2_area, r.boots_jump, one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area + final_pillar_area.connect(bottomleftF2_area, AND(r.sideways_block_push, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD)))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path if options.owlstatues == "both" or options.owlstatues == "dungeon": - bottomleft_owl.connect(bottomleftF2_area, STONE_BEAK7) # sideways block push to get to the owl statue + bottomleft_owl.connect(bottomleftF2_area, AND(r.sideways_block_push, STONE_BEAK7)) # sideways block push to get to the owl statue final_pillar.connect(bottomleftF2_area, BOMB) # bomb trigger pillar - pre_boss.connect(final_pillar, FEATHER) # superjump on top of goomba to extend superjump to boss door plateau + pre_boss.connect(final_pillar, r.super_jump_feather) # superjump on top of goomba to extend superjump to boss door plateau pre_boss.connect(beamos_horseheads_area, None, one_way=True) # can drop down from raised plateau to beamos horseheads area if options.logic == 'hell': - topright_pillar_area.connect(entrance, FEATHER) # superjump in the center to get on raised blocks, has to be low - topright_pillar_area.connect(entrance, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop in the center to get on raised blocks - toprightF1_chest.connect(topright_pillar_area, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop from F1 switch room - pre_boss.connect(final_pillar, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop on top of goomba to extend superhop to boss door plateau + topright_pillar_area.connect(entrance, r.super_jump_feather) # superjump in the center to get on raised blocks, has to be low + topright_pillar_area.connect(entrance, r.boots_superhop) # boots superhop in the center to get on raised blocks + toprightF1_chest.connect(topright_pillar_area, r.boots_superhop) # boots superhop from F1 switch room + pre_boss.connect(final_pillar, r.boots_superhop) # boots superhop on top of goomba to extend superhop to boss door plateau self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon8.py b/worlds/ladx/LADXR/logic/dungeon8.py index 4444ecbb1419..5da2f8234ec4 100644 --- a/worlds/ladx/LADXR/logic/dungeon8.py +++ b/worlds/ladx/LADXR/logic/dungeon8.py @@ -11,7 +11,10 @@ def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): # left side entrance_left.add(DungeonChest(0x24D)) # zamboni room chest - Location(dungeon=8).add(DungeonChest(0x25C)).connect(entrance_left, r.attack_hookshot) # eye magnet chest + eye_magnet_chest = Location(dungeon=8).add(DungeonChest(0x25C)) # eye magnet chest bottom left below rolling bones + eye_magnet_chest.connect(entrance_left, OR(BOW, MAGIC_ROD, BOOMERANG, AND(FEATHER, r.attack_hookshot))) # damageless roller should be default + if options.hardmode != "ohko": + eye_magnet_chest.connect(entrance_left, r.attack_hookshot) # can take a hit vire_drop_key = Location(dungeon=8).add(DroppedKey(0x24C)).connect(entrance_left, r.attack_hookshot_no_bomb) # vire drop key sparks_chest = Location(dungeon=8).add(DungeonChest(0x255)).connect(entrance_left, OR(HOOKSHOT, FEATHER)) # chest before lvl1 miniboss Location(dungeon=8).add(DungeonChest(0x246)).connect(entrance_left, MAGIC_ROD) # key chest that spawns after creating fire @@ -30,7 +33,7 @@ def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): upper_center = Location(dungeon=8).connect(lower_center, AND(KEY8, FOUND(KEY8, 2))) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=8).add(OwlStatue(0x245)).connect(upper_center, STONE_BEAK8) - Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_skeleton) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb + gibdos_drop_key = Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_gibdos) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb medicine_chest = Location(dungeon=8).add(DungeonChest(0x235)).connect(upper_center, AND(FEATHER, HOOKSHOT)) # medicine chest middle_center_1 = Location(dungeon=8).connect(upper_center, BOMB) @@ -66,33 +69,36 @@ def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': entrance_left.connect(entrance, BOMB) # use bombs to kill vire and hinox - vire_drop_key.connect(entrance_left, BOMB) # use bombs to kill rolling bones and vire - bottom_right.connect(slime_chest, FEATHER) # diagonal jump over the pits to reach rolling rock / zamboni + up_left.connect(vire_drop_key, BOMB, one_way=True) # use bombs to kill rolling bones and vire, do not allow pathway through hinox with just bombs, as not enough bombs are available + bottom_right.connect(slime_chest, r.tight_jump) # diagonal jump over the pits to reach rolling rock / zamboni + gibdos_drop_key.connect(upper_center, OR(HOOKSHOT, MAGIC_ROD)) # crack one of the floor tiles and hookshot the gibdos in, or burn the gibdos and make them jump into pit up_left.connect(lower_center, AND(BOMB, FEATHER)) # blow up hidden walls from peahat room -> dark room -> eye statue room slime_chest.connect(entrance, AND(r.attack_hookshot_powder, POWER_BRACELET)) # kill vire with powder or bombs if options.logic == 'glitched' or options.logic == 'hell': - sparks_chest.connect(entrance_left, OR(r.attack_hookshot, FEATHER, PEGASUS_BOOTS)) # 1 pit buffer across the pit. Add requirements for all the options to get to this area - lower_center.connect(entrance_up, None) # sideways block push in peahat room to get past keyblock - miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, HOOKSHOT)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs - miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock - up_left.connect(lower_center, FEATHER) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump - up_left.connect(upper_center, FEATHER) # from up left you can jesus jump / lava swim around the key door next to the boss. - top_left_stairs.connect(up_left, AND(FEATHER, SWORD)) # superjump - medicine_chest.connect(upper_center, FEATHER) # jesus super jump - up_left.connect(bossdoor, FEATHER, one_way=True) # superjump off the bottom or right wall to jump over to the boss door + sparks_chest.connect(entrance_left, r.pit_buffer_itemless) # 1 pit buffer across the pit. + entrance_up.connect(bottomright_pot_chest, r.super_jump_boots, one_way = True) # underground section with fire balls jumping up out of lava. Use boots superjump off left wall to jump over the pot blocking the way + lower_center.connect(entrance_up, r.sideways_block_push) # sideways block push in peahat room to get past keyblock + miniboss_entrance.connect(lower_center, AND(BOMB, r.bookshot)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs + miniboss_entrance.connect(lower_center, AND(BOMB, r.super_jump_feather, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock + up_left.connect(lower_center, r.jesus_jump) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump + up_left.connect(upper_center, r.jesus_jump) # from up left you can jesus jump / lava swim around the key door next to the boss. + top_left_stairs.connect(up_left, r.super_jump_feather) # superjump + medicine_chest.connect(upper_center, AND(r.super_jump_feather, r.jesus_jump)) # jesus super jump + up_left.connect(bossdoor, r.super_jump_feather, one_way=True) # superjump off the bottom or right wall to jump over to the boss door if options.logic == 'hell': if bottomright_owl: - bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder - bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS)) # underground section past mimics, boots bonking across the gap to the ladder - entrance.connect(bottomright_pot_chest, AND(FEATHER, SWORD), one_way=True) # use NW zamboni staircase backwards, subpixel manip for superjump past the pots - medicine_chest.connect(upper_center, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section - miniboss.connect(miniboss_entrance, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks - top_left_stairs.connect(map_chest, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section - nightmare_key.connect(top_left_stairs, AND(PEGASUS_BOOTS, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room - bottom_right.connect(entrance_up, AND(POWER_BRACELET, PEGASUS_BOOTS), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni - bossdoor.connect(entrance_up, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk through 2d section + bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, r.boots_bonk_2d_hell, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder + bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, r.boots_bonk_2d_hell)) # underground section past mimics, boots bonking across the gap to the ladder + entrance.connect(bottomright_pot_chest, r.shaq_jump, one_way=True) # use NW zamboni staircase backwards, and get a naked shaq jump off the bottom wall in the bottom right corner to pass by the pot + gibdos_drop_key.connect(upper_center, AND(FEATHER, SHIELD)) # lock gibdos into pits and crack the tile they stand on, then use shield to bump them into the pit + medicine_chest.connect(upper_center, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section + miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks + top_left_stairs.connect(map_chest, AND(r.jesus_buffer, r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section + nightmare_key.connect(top_left_stairs, AND(r.boots_bonk_pit, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room + bottom_right.connect(entrance_up, AND(POWER_BRACELET, r.jesus_buffer), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni + bossdoor.connect(entrance_up, AND(r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk through 2d section self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeonColor.py b/worlds/ladx/LADXR/logic/dungeonColor.py index aa58c0bafa91..fc14f70dd7a6 100644 --- a/worlds/ladx/LADXR/logic/dungeonColor.py +++ b/worlds/ladx/LADXR/logic/dungeonColor.py @@ -10,7 +10,7 @@ def __init__(self, options, world_setup, r): room2.add(DungeonChest(0x314)) # key if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=9).add(OwlStatue(0x308), OwlStatue(0x30F)).connect(room2, STONE_BEAK9) - room2_weapon = Location(dungeon=9).connect(room2, r.attack_hookshot) + room2_weapon = Location(dungeon=9).connect(room2, AND(r.attack_hookshot, POWER_BRACELET)) room2_weapon.add(DungeonChest(0x311)) # stone beak room2_lights = Location(dungeon=9).connect(room2, OR(r.attack_hookshot, SHIELD)) room2_lights.add(DungeonChest(0x30F)) # compass chest @@ -20,22 +20,24 @@ def __init__(self, options, world_setup, r): room3 = Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 2), r.miniboss_requirements[world_setup.miniboss_mapping["c1"]])) # After the miniboss room4 = Location(dungeon=9).connect(room3, POWER_BRACELET) # need to lift a pot to reveal button room4.add(DungeonChest(0x306)) # map - room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, r.attack_hookshot) # require item to knock Karakoro enemies into shell + room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, AND(r.attack_hookshot, POWER_BRACELET)) # require item to knock Karakoro enemies into shell if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=9).add(OwlStatue(0x30A)).connect(room4, STONE_BEAK9) room5 = Location(dungeon=9).connect(room4, OR(r.attack_hookshot, SHIELD)) # lights room room6 = Location(dungeon=9).connect(room5, AND(KEY9, FOUND(KEY9, 3))) # room with switch and nightmare door - pre_boss = Location(dungeon=9).connect(room6, OR(r.attack_hookshot, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks + pre_boss = Location(dungeon=9).connect(room6, OR(r.hit_switch, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks boss = Location(dungeon=9).connect(pre_boss, AND(NIGHTMARE_KEY9, r.boss_requirements[world_setup.boss_mapping[8]])) boss.add(TunicFairy(0), TunicFairy(1)) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': - room2.connect(entrance, POWER_BRACELET) # throw pots at enemies - pre_boss.connect(room6, FEATHER) # before the boss, jump past raised blocks without boots + room2.connect(entrance, r.throw_pot) # throw pots at enemies + room2_weapon.connect(room2, r.attack_hookshot_no_bomb) # knock the karakoro into the pit without picking them up. + pre_boss.connect(room6, r.tight_jump) # before the boss, jump past raised blocks without boots if options.logic == 'hell': - room2_weapon.connect(room2, SHIELD) # shield bump karakoro into the holes - room4karakoro.connect(room4, SHIELD) # shield bump karakoro into the holes + room2_weapon.connect(room2, r.attack_hookshot) # also have a bomb as option to knock the karakoro into the pit without bracelet + room2_weapon.connect(room2, r.shield_bump) # shield bump karakoro into the holes + room4karakoro.connect(room4, r.shield_bump) # shield bump karakoro into the holes self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index 3972796051f9..54da90f8931d 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -19,10 +19,13 @@ def __init__(self, options, world_setup, r): Location().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3)) papahl_house = Location("Papahl House") - papahl_house.connect(Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL) + mamasha_trade = Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)) + papahl_house.connect(mamasha_trade, TRADING_ITEM_YOSHI_DOLL) - trendy_shop = Location("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)) - #trendy_shop.connect(Location()) + trendy_shop = Location("Trendy Shop") + trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50)) + outside_trendy = Location() + outside_trendy.connect(mabe_village, r.bush) self._addEntrance("papahl_house_left", mabe_village, papahl_house, None) self._addEntrance("papahl_house_right", mabe_village, papahl_house, None) @@ -84,7 +87,7 @@ def __init__(self, options, world_setup, r): crazy_tracy_hut_inside = Location("Crazy Tracy's House") Location().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50)) self._addEntrance("crazy_tracy", crazy_tracy_hut, crazy_tracy_hut_inside, None) - start_house.connect(crazy_tracy_hut, SONG2, one_way=True) # Manbo's Mambo into the pond outside Tracy + start_house.connect(crazy_tracy_hut, AND(OCARINA, SONG2), one_way=True) # Manbo's Mambo into the pond outside Tracy forest_madbatter = Location("Forest Mad Batter") Location().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER) @@ -92,7 +95,7 @@ def __init__(self, options, world_setup, r): self._addEntranceRequirementExit("forest_madbatter", None) # if exiting, you do not need bracelet forest_cave = Location("Forest Cave") - Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom + forest_cave_crystal_chest = Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom log_cave_heartpiece = Location().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom forest_toadstool = Location().add(Toadstool()) self._addEntrance("toadstool_entrance", forest, forest_cave, None) @@ -130,6 +133,7 @@ def __init__(self, options, world_setup, r): self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet ghost_grave = Location().connect(forest, POWER_BRACELET) Location().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot + graveyard.connect(forest_heartpiece, OR(BOOMERANG, HOOKSHOT), one_way=True) # grab the heart piece surrounded by pits from the north graveyard_cave_left = Location() graveyard_cave_right = Location().connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) @@ -194,6 +198,7 @@ def __init__(self, options, world_setup, r): bay_madbatter_connector_exit = Location().connect(bay_madbatter_connector_entrance, FLIPPERS) bay_madbatter_connector_outside = Location() bay_madbatter = Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + outside_bay_madbatter_entrance = Location() self._addEntrance("prairie_madbatter_connector_entrance", left_bay_area, bay_madbatter_connector_entrance, AND(OR(FEATHER, ROOSTER), OR(SWORD, MAGIC_ROD, BOOMERANG))) self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), r.bush)) # if exiting, you can pick up the bushes by normal means self._addEntrance("prairie_madbatter_connector_exit", bay_madbatter_connector_outside, bay_madbatter_connector_exit, None) @@ -239,7 +244,8 @@ def __init__(self, options, world_setup, r): castle_courtyard = Location() castle_frontdoor = Location().connect(castle_courtyard, r.bush) castle_frontdoor.connect(ukuku_prairie, "CASTLE_BUTTON") # the button in the castle connector allows access to the castle grounds in ER - self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, r.pit_bush) + self._addEntranceRequirementExit("castle_secret_entrance", None) # leaving doesn't require pit_bush self._addEntrance("castle_secret_exit", castle_courtyard, castle_secret_entrance_left, None) Location().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle @@ -247,7 +253,7 @@ def __init__(self, options, world_setup, r): Location().add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None) castle_top_outside = Location() castle_top_inside = Location() - self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, r.bush) + self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, None) self._addEntrance("castle_upper_left", castle_top_outside, castle_inside, None) self._addEntrance("castle_upper_right", castle_top_outside, castle_top_inside, None) Location().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes @@ -276,7 +282,8 @@ def __init__(self, options, world_setup, r): animal_village.connect(ukuku_prairie, OR(HOOKSHOT, ROOSTER)) animal_village_connector_left = Location() animal_village_connector_right = Location().connect(animal_village_connector_left, PEGASUS_BOOTS) - self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) # passage under river blocked by bush + self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, r.pit_bush) # passage under river blocked by bush + self._addEntranceRequirementExit("prairie_to_animal_connector", None) # leaving doesn't require pit_bush self._addEntrance("animal_to_prairie_connector", animal_village, animal_village_connector_right, None) if options.owlstatues == "both" or options.owlstatues == "overworld": animal_village.add(OwlStatue(0x0DA)) @@ -284,7 +291,7 @@ def __init__(self, options, world_setup, r): desert = Location().connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert. if options.owlstatues == "both" or options.owlstatues == "overworld": desert.add(OwlStatue(0x0CF)) - desert_lanmola = Location().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + desert_lanmola = Location().add(AnglerKey()).connect(desert, r.attack_hookshot_no_bomb) animal_village_bombcave = Location() self._addEntrance("animal_cave", desert, animal_village_bombcave, BOMB) @@ -298,13 +305,15 @@ def __init__(self, options, world_setup, r): Location().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave Location().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map - armos_maze = Location().connect(animal_village, POWER_BRACELET) - armos_temple = Location() + armos_maze = Location("Armos Maze").connect(animal_village, POWER_BRACELET) + armos_temple = Location("Southern Shrine") Location().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]]) if options.owlstatues == "both" or options.owlstatues == "overworld": armos_maze.add(OwlStatue(0x08F)) - self._addEntrance("armos_maze_cave", armos_maze, Location().add(Chest(0x2FC)), None) - self._addEntrance("armos_temple", armos_maze, armos_temple, None) + outside_armos_cave = Location("Outside Armos Maze Cave").connect(armos_maze, OR(r.attack_hookshot, SHIELD)) + outside_armos_temple = Location("Outside Southern Shrine").connect(armos_maze, OR(r.attack_hookshot, SHIELD)) + self._addEntrance("armos_maze_cave", outside_armos_cave, Location().add(Chest(0x2FC)), None) + self._addEntrance("armos_temple", outside_armos_temple, armos_temple, None) armos_fairy_entrance = Location().connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET) self._addEntrance("armos_fairy", armos_fairy_entrance, None, BOMB) @@ -349,17 +358,21 @@ def __init__(self, options, world_setup, r): lower_right_taltal.connect(below_right_taltal, FLIPPERS, one_way=True) heartpiece_swim_cave = Location().connect(Location().add(HeartPiece(0x1F2)), FLIPPERS) + outside_swim_cave = Location() + below_right_taltal.connect(outside_swim_cave, FLIPPERS) self._addEntrance("heartpiece_swim_cave", below_right_taltal, heartpiece_swim_cave, FLIPPERS) # cave next to level 4 d4_entrance = Location().connect(below_right_taltal, FLIPPERS) lower_right_taltal.connect(d4_entrance, AND(ANGLER_KEY, "ANGLER_KEYHOLE"), one_way=True) self._addEntrance("d4", d4_entrance, None, ANGLER_KEY) self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon + outside_mambo = Location("Outside Manbo").connect(d4_entrance, FLIPPERS) + inside_mambo = Location("Manbo's Cave") mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) # Raft game. raft_house = Location("Raft House") - Location().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100)) + Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.bush, COUNT("RUPEES", 100))) # add bush requirement for farming in case player has to try again raft_return_upper = Location() raft_return_lower = Location().connect(raft_return_upper, None, one_way=True) outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True) @@ -391,10 +404,13 @@ def __init__(self, options, world_setup, r): multichest_cave = Location() multichest_cave_secret = Location().connect(multichest_cave, BOMB) + multichest_cave.connect(multichest_cave_secret, BOMB, one_way=True) water_cave_hole = Location() # Location with the hole that drops you onto the hearth piece under water if options.logic != "casual": water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) + outside_multichest_left = Location() multichest_outside = Location().add(Chest(0x01D)) # chest after multichest puzzle outside + lower_right_taltal.connect(outside_multichest_left, OR(FLIPPERS, ROOSTER)) self._addEntrance("multichest_left", lower_right_taltal, multichest_cave, OR(FLIPPERS, ROOSTER)) self._addEntrance("multichest_right", water_cave_hole, multichest_cave, None) self._addEntrance("multichest_top", multichest_outside, multichest_cave_secret, None) @@ -432,7 +448,7 @@ def __init__(self, options, world_setup, r): left_right_connector_cave_exit = Location() left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, OR(HOOKSHOT, ROOSTER), one_way=True) # pass through the underground passage to left side taltal_boulder_zone = Location() - self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, r.pit_bush) self._addEntrance("left_taltal_entrance", taltal_boulder_zone, left_right_connector_cave_exit, None) mountain_heartpiece = Location().add(HeartPiece(0x2BA)) # heartpiece in connecting cave left_right_connector_cave_entrance.connect(mountain_heartpiece, BOMB, one_way=True) # in the connecting cave from right to left. one_way to prevent access to left_side_mountain via glitched logic @@ -464,130 +480,168 @@ def __init__(self, options, world_setup, r): windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': - hookshot_cave.connect(hookshot_cave_chest, AND(FEATHER, PEGASUS_BOOTS)) # boots jump the gap to the chest - graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT, one_way=True) # hookshot the block behind the stairs while over the pit - swamp_chest.connect(swamp, None) # Clip past the flower + hookshot_cave.connect(hookshot_cave_chest, r.boots_jump) # boots jump the gap to the chest + graveyard_cave_left.connect(graveyard_cave_right, r.hookshot_over_pit, one_way=True) # hookshot the block behind the stairs while over the pit + swamp_chest.connect(swamp, r.wall_clip) # Clip past the flower self._addEntranceRequirement("d2", POWER_BRACELET) # clip the top wall to walk between the goponga flower and the wall self._addEntranceRequirement("d2", COUNT(SWORD, 2)) # use l2 sword spin to kill goponga flowers - swamp.connect(writes_hut_outside, HOOKSHOT, one_way=True) # hookshot the sign in front of writes hut + self._addEntranceRequirementExit("d2", r.wall_clip) # Clip out at d2 entrance door + swamp.connect(writes_hut_outside, r.hookshot_over_pit, one_way=True) # hookshot the sign in front of writes hut graveyard_heartpiece.connect(graveyard_cave_right, FEATHER) # jump to the bottom right tile around the blocks - graveyard_heartpiece.connect(graveyard_cave_right, OR(HOOKSHOT, BOOMERANG)) # push bottom block, wall clip and hookshot/boomerang corner to grab item + graveyard_heartpiece.connect(graveyard_cave_right, AND(r.wall_clip, OR(HOOKSHOT, BOOMERANG))) # push bottom block, wall clip and hookshot/boomerang corner to grab item - self._addEntranceRequirement("mamu", AND(FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped + self._addEntranceRequirement("mamu", AND(r.wall_clip, FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), OR(MAGIC_POWDER, BOMB))) # use bombs or powder to get rid of a bush on the other side by jumping across and placing the bomb/powder before you fall into the pit - fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # can talk to the fisherman from the water when the boat is low (requires swimming up out of the water a bit) crow_gold_leaf.connect(castle_courtyard, POWER_BRACELET) # bird on tree at left side kanalet, can use both rocks to kill the crow removing the kill requirement castle_inside.connect(kanalet_chain_trooper, BOOMERANG, one_way=True) # kill the ball and chain trooper from the left side, then use boomerang to grab the dropped item - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(PEGASUS_BOOTS, FEATHER)) # jump across horizontal 4 gap to heart piece + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, r.boots_jump) # jump across horizontal 4 gap to heart piece + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, FEATHER, BOOMERANG)) # use jump + boomerang to grab the item from below the ledge desert_lanmola.connect(desert, BOMB) # use bombs to kill lanmola + armos_maze.connect(outside_armos_cave, None) # dodge the armos statues by activating them and running + armos_maze.connect(outside_armos_temple, None) # dodge the armos statues by activating them and running d6_connector_left.connect(d6_connector_right, AND(OR(FLIPPERS, PEGASUS_BOOTS), FEATHER)) # jump the gap in underground passage to d6 left side to skip hookshot + obstacle_cave_exit.connect(obstacle_cave_inside, AND(FEATHER, r.hookshot_over_pit), one_way=True) # one way from right exit to middle, jump past the obstacle, and use hookshot to pull past the double obstacle if not options.rooster: bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue - fire_cave_bottom.connect(fire_cave_top, PEGASUS_BOOTS, one_way=True) # flame skip + right_taltal_connector2.connect(right_taltal_connector3, ROOSTER, one_way=True) # jump off the ledge and grab rooster after landing on the pit + fire_cave_bottom.connect(fire_cave_top, AND(r.damage_boost_special, PEGASUS_BOOTS), one_way=True) # flame skip if options.logic == 'glitched' or options.logic == 'hell': + papahl_house.connect(mamasha_trade, r.bomb_trigger) # use a bomb trigger to trade with mamasha without having yoshi doll #self._addEntranceRequirement("dream_hut", FEATHER) # text clip TODO: require nag messages - self._addEntranceRequirementEnter("dream_hut", HOOKSHOT) # clip past the rocks in front of dream hut - dream_hut_right.connect(dream_hut_left, FEATHER) # super jump - forest.connect(swamp, BOMB) # bomb trigger tarin + self._addEntranceRequirementEnter("dream_hut", r.hookshot_clip) # clip past the rocks in front of dream hut + dream_hut_right.connect(dream_hut_left, r.super_jump_feather) # super jump + forest.connect(swamp, r.bomb_trigger) # bomb trigger tarin forest.connect(forest_heartpiece, BOMB, one_way=True) # bomb trigger heartpiece - self._addEntranceRequirementEnter("hookshot_cave", HOOKSHOT) # clip past the rocks in front of hookshot cave - swamp.connect(forest_toadstool, None, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area) - writes_hut_outside.connect(swamp, None, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost + self._addEntranceRequirementEnter("hookshot_cave", r.hookshot_clip) # clip past the rocks in front of hookshot cave + swamp.connect(forest_toadstool, r.pit_buffer_itemless, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area) + writes_hut_outside.connect(swamp, r.pit_buffer_itemless, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost graveyard.connect(forest_heartpiece, None, one_way=True) # villa buffer from top. - log_cave_heartpiece.connect(forest_cave, FEATHER) # super jump - log_cave_heartpiece.connect(forest_cave, BOMB) # bomb trigger - graveyard_cave_left.connect(graveyard_heartpiece, BOMB, one_way=True) # bomb trigger the heartpiece from the left side - graveyard_heartpiece.connect(graveyard_cave_right, None) # sideways block push from the right staircase. - - prairie_island_seashell.connect(ukuku_prairie, AND(FEATHER, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island - self._addEntranceRequirement("castle_jump_cave", FEATHER) # 1 pit buffer to clip bottom wall and jump across. - left_bay_area.connect(ghost_hut_outside, FEATHER) # 1 pit buffer to get across - tiny_island.connect(left_bay_area, AND(FEATHER, r.bush)) # jesus jump around - bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter - self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up - - ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze - fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook - animal_village.connect(ukuku_prairie, FEATHER) # jesus jump - below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth) - animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, OR(HOOKSHOT, FEATHER, PEGASUS_BOOTS))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, FEATHER) # villa buffer across the pits - - d6_entrance.connect(ukuku_prairie, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie - d6_entrance.connect(armos_fairy_entrance, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance - armos_fairy_entrance.connect(d6_armos_island, FEATHER, one_way=True) # jesus jump from top (fairy bomb cave) to armos island - armos_fairy_entrance.connect(raft_exit, FEATHER) # jesus jump (2-ish screen) from fairy cave to lower raft connector - self._addEntranceRequirementEnter("obstacle_cave_entrance", HOOKSHOT) # clip past the rocks in front of obstacle cave entrance - obstacle_cave_inside_chest.connect(obstacle_cave_inside, FEATHER) # jump to the rightmost pits + 1 pit buffer to jump across - obstacle_cave_exit.connect(obstacle_cave_inside, FEATHER) # 1 pit buffer above boots crystals to get past - lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, BOMB), one_way=True) # bomb trigger papahl from below ledge, requires pineapple - - self._addEntranceRequirement("heartpiece_swim_cave", FEATHER) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below - self._addEntranceRequirement("mambo", FEATHER) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance - outside_raft_house.connect(below_right_taltal, FEATHER, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south - - self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain - outside_rooster_house.connect(lower_right_taltal, FEATHER) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain + graveyard.connect(forest, None, one_way=True) # villa buffer from the top twice to get to the main forest area + log_cave_heartpiece.connect(forest_cave, r.super_jump_feather) # super jump + log_cave_heartpiece.connect(forest_cave, r.bomb_trigger) # bomb trigger + graveyard_cave_left.connect(graveyard_heartpiece, r.bomb_trigger, one_way=True) # bomb trigger the heartpiece from the left side + graveyard_heartpiece.connect(graveyard_cave_right, r.sideways_block_push) # sideways block push from the right staircase. + + prairie_island_seashell.connect(ukuku_prairie, AND(r.jesus_jump, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island + self._addEntranceRequirement("castle_jump_cave", r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across. + left_bay_area.connect(ghost_hut_outside, r.pit_buffer) # 1 pit buffer to get across + tiny_island.connect(left_bay_area, AND(r.jesus_jump, r.bush)) # jesus jump around + bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, r.jesus_jump, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter + left_bay_area.connect(outside_bay_madbatter_entrance, AND(r.pit_buffer, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up + + ukuku_prairie.connect(richard_maze, AND(r.pit_buffer_itemless, OR(AND(MAGIC_POWDER, MAX_POWDER_UPGRADE), BOMB, BOOMERANG, MAGIC_ROD, SWORD)), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze + richard_maze.connect(ukuku_prairie, AND(r.pit_buffer_itemless, OR(MAGIC_POWDER, BOMB, BOOMERANG, MAGIC_ROD, SWORD)), one_way=True) # same as above (without powder upgrade) in one of the two northern screens of the maze to escape + fisher_under_bridge.connect(bay_water, AND(r.bomb_trigger, AND(FEATHER, FLIPPERS))) # up-most left wall is a pit: bomb trigger with it. If photographer is there, clear that first which is why feather is required logically + animal_village.connect(ukuku_prairie, r.jesus_jump) # jesus jump + below_right_taltal.connect(next_to_castle, r.jesus_jump) # jesus jump (north of kanalet castle phonebooth) + #animal_village_connector_right.connect(animal_village_connector_left, AND(r.text_clip, FEATHER)) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(r.bomb_trigger, OR(HOOKSHOT, FEATHER, r.boots_bonk_pit))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, r.pit_buffer) # villa buffer across the pits + + d6_entrance.connect(ukuku_prairie, r.jesus_jump, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie + d6_entrance.connect(armos_fairy_entrance, r.jesus_jump, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance + d6_connector_left.connect(d6_connector_right, r.jesus_jump) # jesus jump over water; left side is jumpable, or villa buffer if it's easier for you + armos_fairy_entrance.connect(d6_armos_island, r.jesus_jump, one_way=True) # jesus jump from top (fairy bomb cave) to armos island + armos_fairy_entrance.connect(raft_exit, r.jesus_jump) # jesus jump (2-ish screen) from fairy cave to lower raft connector + self._addEntranceRequirementEnter("obstacle_cave_entrance", r.hookshot_clip) # clip past the rocks in front of obstacle cave entrance + obstacle_cave_inside_chest.connect(obstacle_cave_inside, r.pit_buffer) # jump to the rightmost pits + 1 pit buffer to jump across + obstacle_cave_exit.connect(obstacle_cave_inside, r.pit_buffer) # 1 pit buffer above boots crystals to get past + lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, r.bomb_trigger), one_way=True) # bomb trigger papahl from below ledge, requires pineapple + + self._addEntranceRequirement("heartpiece_swim_cave", r.jesus_jump) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below + self._addEntranceRequirement("mambo", r.jesus_jump) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance + outside_raft_house.connect(below_right_taltal, r.jesus_jump, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south + + self._addEntranceRequirement("multichest_left", r.jesus_jump) # jesus jump past staircase leading up the mountain + outside_rooster_house.connect(lower_right_taltal, r.jesus_jump) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain d7_platau.connect(water_cave_hole, None, one_way=True) # use save and quit menu to gain control while falling to dodge the water cave hole - mountain_bridge_staircase.connect(outside_rooster_house, AND(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across - bird_key.connect(bird_cave, AND(FEATHER, HOOKSHOT)) # hookshot jump across the big pits room - right_taltal_connector2.connect(right_taltal_connector3, None, one_way=True) # 2 seperate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen - left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, FEATHER), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end - obstacle_cave_inside.connect(mountain_heartpiece, BOMB, one_way=True) # bomb trigger from boots crystal cave - self._addEntranceRequirement("d8", OR(BOMB, AND(OCARINA, SONG3))) # bomb trigger the head and walk trough, or play the ocarina song 3 and walk through + mountain_bridge_staircase.connect(outside_rooster_house, AND(r.boots_jump, r.pit_buffer)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across. added boots_jump to not require going through this section with just feather + bird_key.connect(bird_cave, r.hookshot_jump) # hookshot jump across the big pits room + right_taltal_connector2.connect(right_taltal_connector3, OR(r.pit_buffer, ROOSTER), one_way=True) # trigger a quick fall on the screen above the exit by transitioning down on the leftmost/rightmost pit and then buffering sq menu for control while in the air. or pick up the rooster while dropping off the ledge at exit + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, r.super_jump_feather), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end + obstacle_cave_inside.connect(mountain_heartpiece, r.bomb_trigger, one_way=True) # bomb trigger from boots crystal cave + self._addEntranceRequirement("d8", OR(r.bomb_trigger, AND(OCARINA, SONG3))) # bomb trigger the head and walk through, or play the ocarina song 3 and walk through if options.logic == 'hell': dream_hut_right.connect(dream_hut, None) # alternate diagonal movement with orthogonal movement to control the mimics. Get them clipped into the walls to walk past - swamp.connect(forest_toadstool, None) # damage boost from toadstool area across the pit - swamp.connect(forest, AND(r.bush, OR(PEGASUS_BOOTS, HOOKSHOT))) # boots bonk / hookshot spam over the pits right of forest_rear_chest + swamp.connect(forest_toadstool, r.damage_boost) # damage boost from toadstool area across the pit + swamp.connect(forest, AND(r.bush, OR(r.boots_bonk_pit, r.hookshot_spam_pit))) # boots bonk / hookshot spam over the pits right of forest_rear_chest forest.connect(forest_heartpiece, PEGASUS_BOOTS, one_way=True) # boots bonk across the pits + forest_cave_crystal_chest.connect(forest_cave, AND(r.super_jump_feather, r.hookshot_clip_block, r.sideways_block_push)) # superjump off the bottom wall to get between block and crystal, than use 3 keese to hookshot clip while facing right to get a sideways blockpush off log_cave_heartpiece.connect(forest_cave, BOOMERANG) # clip the boomerang through the corner gaps on top right to grab the item - log_cave_heartpiece.connect(forest_cave, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD))) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up - writes_hut_outside.connect(swamp, None) # damage boost with moblin arrow next to telephone booth - writes_cave_left_chest.connect(writes_cave, None) # damage boost off the zol to get across the pit. - graveyard.connect(crazy_tracy_hut, HOOKSHOT, one_way=True) # use hookshot spam to clip the rock on the right with the crow - graveyard.connect(forest, OR(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk witches hut, or hookshot spam across the pit - graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT) # hookshot spam over the pit - graveyard_cave_right.connect(graveyard_cave_left, PEGASUS_BOOTS, one_way=True) # boots bonk off the cracked block - - self._addEntranceRequirementEnter("mamu", AND(PEGASUS_BOOTS, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall - self._addEntranceRequirement("castle_jump_cave", PEGASUS_BOOTS) # pit buffer to clip bottom wall and boots bonk across - prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(PEGASUS_BOOTS, HOOKSHOT))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across - richard_cave_chest.connect(richard_cave, None) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol) - castle_secret_entrance_right.connect(castle_secret_entrance_left, AND(PEGASUS_BOOTS, "MEDICINE2")) # medicine iframe abuse to get across spikes with a boots bonk - left_bay_area.connect(ghost_hut_outside, PEGASUS_BOOTS) # multiple pit buffers to bonk across the bottom wall - tiny_island.connect(left_bay_area, AND(PEGASUS_BOOTS, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly) - self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, OR(MAGIC_POWDER, BOMB, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land - self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, r.bush)) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall + log_cave_heartpiece.connect(forest_cave, OR(r.super_jump_rooster, r.boots_roosterhop)) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up + writes_hut_outside.connect(swamp, r.damage_boost) # damage boost with moblin arrow next to telephone booth + writes_cave_left_chest.connect(writes_cave, r.damage_boost) # damage boost off the zol to get across the pit. + graveyard.connect(crazy_tracy_hut, r.hookshot_spam_pit, one_way=True) # use hookshot spam to clip the rock on the right with the crow + graveyard.connect(forest, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk over pits by witches hut, or hookshot spam across the pit + graveyard_cave_left.connect(graveyard_cave_right, r.hookshot_spam_pit) # hookshot spam over the pit + graveyard_cave_right.connect(graveyard_cave_left, OR(r.damage_boost, r.boots_bonk_pit), one_way=True) # boots bonk off the cracked block, or set up a damage boost with the keese + + self._addEntranceRequirementEnter("mamu", AND(r.pit_buffer_itemless, r.pit_buffer_boots, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall + self._addEntranceRequirement("castle_jump_cave", r.pit_buffer_boots) # pit buffer to clip bottom wall and boots bonk across + prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(r.boots_bonk_pit, r.hookshot_spam_pit))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across + richard_cave_chest.connect(richard_cave, r.damage_boost) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol) + castle_secret_entrance_right.connect(castle_secret_entrance_left, r.boots_bonk_2d_spikepit) # medicine iframe abuse to get across spikes with a boots bonk + left_bay_area.connect(ghost_hut_outside, r.pit_buffer_boots) # multiple pit buffers to bonk across the bottom wall + left_bay_area.connect(ukuku_prairie, r.hookshot_clip_block, one_way=True) # clip through the donuts blocking the path next to prairie plateau cave by hookshotting up and killing the two moblins that way which clips you further up two times. This is enough to move right + tiny_island.connect(left_bay_area, AND(r.jesus_buffer, r.boots_bonk_pit, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly) + left_bay_area.connect(outside_bay_madbatter_entrance, AND(r.pit_buffer_boots, OR(MAGIC_POWDER, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land + left_bay_area.connect(outside_bay_madbatter_entrance, AND(r.pit_buffer, r.hookshot_spam_pit, r.bush)) # hookshot spam to cross one pit at the top, then buffer until on top of the bush to be able to break it + outside_bay_madbatter_entrance.connect(left_bay_area, AND(r.pit_buffer_boots, r.bush), one_way=True) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall # bay_water connectors, only left_bay_area, ukuku_prairie and animal_village have to be connected with jesus jumps. below_right_taltal, d6_armos_island and armos_fairy_entrance are accounted for via ukuku prairie in glitch logic - left_bay_area.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) - animal_village.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) - ukuku_prairie.connect(bay_water, FEATHER, one_way=True) # jesus jump - bay_water.connect(d5_entrance, FEATHER) # jesus jump into d5 entrance (wall clip), wall clip + jesus jump to get out + left_bay_area.connect(bay_water, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump/rooster (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + animal_village.connect(bay_water, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump/rooster (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + ukuku_prairie.connect(bay_water, OR(r.jesus_jump, r.jesus_rooster), one_way=True) # jesus jump/rooster + bay_water.connect(d5_entrance, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump/rooster into d5 entrance (wall clip), wall clip + jesus jump to get out - crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed - mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, FEATHER)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, PEGASUS_BOOTS) # boots bonk across bottom wall (both at entrance and in item room) + prairie_island_seashell.connect(ukuku_prairie, AND(r.jesus_rooster, r.bush)) # jesus rooster from right side, screen transition on top of the water to reach the island + bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, r.jesus_rooster, one_way=True) # jesus rooster (3 screen) through the underground passage leading to martha's bay mad batter + # fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, OR(FEATHER, SWORD, BOW), FLIPPERS)) # just swing/shoot at fisher, if photographer is on screen it is dumb + fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # face the fisherman from the left, get within 4 pixels (a range, not exact) of his left side, hold up, and mash a until you get the textbox. + + #TODO: add jesus rooster to trick list - d6_armos_island.connect(ukuku_prairie, FEATHER) # jesus jump (3 screen) from seashell mansion to armos island - armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling - d6_connector_right.connect(d6_connector_left, PEGASUS_BOOTS) # boots bonk across bottom wall at water and pits (can do both ways) + below_right_taltal.connect(next_to_castle, r.jesus_buffer, one_way=True) # face right, boots bonk and get far enough left to jesus buffer / boots bonk across the bottom wall to the stairs + crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed + mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, r.super_jump_feather)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) + animal_village_connector_right.connect(animal_village_connector_left, r.shaq_jump) # shaq jump off the obstacle to get through + animal_village_connector_left.connect(animal_village_connector_right, r.hookshot_clip_block, one_way=True) # use hookshot with an enemy to clip through the obstacle + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, r.pit_buffer_boots) # boots bonk across bottom wall (both at entrance and in item room) + + d6_armos_island.connect(ukuku_prairie, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump / rooster (3 screen) from seashell mansion to armos island + armos_fairy_entrance.connect(d6_armos_island, r.jesus_buffer, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling + d6_connector_right.connect(d6_connector_left, r.pit_buffer_boots) # boots bonk across bottom wall at water and pits (can do both ways) + d6_entrance.connect(ukuku_prairie, r.jesus_rooster, one_way=True) # jesus rooster (2 screen) from d6 entrance bottom ledge to ukuku prairie + d6_entrance.connect(armos_fairy_entrance, r.jesus_rooster, one_way=True) # jesus rooster (2 screen) from d6 entrance top ledge to armos fairy entrance + armos_fairy_entrance.connect(d6_armos_island, r.jesus_rooster, one_way=True) # jesus rooster from top (fairy bomb cave) to armos island + armos_fairy_entrance.connect(raft_exit, r.jesus_rooster) # jesus rooster (2-ish screen) from fairy cave to lower raft connector + + obstacle_cave_entrance.connect(obstacle_cave_inside, OR(r.hookshot_clip_block, r.shaq_jump)) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down + obstacle_cave_entrance.connect(obstacle_cave_inside, r.boots_roosterhop) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall + d4_entrance.connect(below_right_taltal, OR(r.jesus_jump, r.jesus_rooster), one_way=True) # jesus jump/rooster 5 screens to staircase below damp cave + lower_right_taltal.connect(below_right_taltal, OR(r.jesus_jump, r.jesus_rooster), one_way=True) # jesus jump/rooster to upper ledges, jump off, enter and exit s+q menu to regain pauses, then jesus jump 4 screens to staircase below damp cave + below_right_taltal.connect(outside_swim_cave, r.jesus_rooster) # jesus rooster into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below + outside_mambo.connect(below_right_taltal, OR(r.jesus_rooster, r.jesus_jump)) # jesus jump/rooster to mambo's cave entrance + if options.hardmode != "oracle": # don't take damage from drowning in water. Could get it with more health probably but standard 3 hearts is not enough + mambo.connect(inside_mambo, AND(OCARINA, r.bomb_trigger)) # while drowning, buffer a bomb and after it explodes, buffer another bomb out of the save and quit menu. + outside_raft_house.connect(below_right_taltal, r.jesus_rooster, one_way=True) # jesus rooster from the ledge at raft to the staircase 1 screen south + lower_right_taltal.connect(outside_multichest_left, r.jesus_rooster) # jesus rooster past staircase leading up the mountain + outside_rooster_house.connect(lower_right_taltal, r.jesus_rooster, one_way=True) # jesus rooster down to staircase below damp cave - obstacle_cave_entrance.connect(obstacle_cave_inside, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS, OR(SWORD, MAGIC_ROD, BOW)))) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down - obstacle_cave_entrance.connect(obstacle_cave_inside, AND(PEGASUS_BOOTS, ROOSTER)) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall - d4_entrance.connect(below_right_taltal, FEATHER) # jesus jump a long way if options.entranceshuffle in ("default", "simple"): # connector cave from armos d6 area to raft shop may not be randomized to add a flippers path since flippers stop you from jesus jumping - below_right_taltal.connect(raft_game, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario) - outside_raft_house.connect(below_right_taltal, AND(FEATHER, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect - bridge_seashell.connect(outside_rooster_house, AND(PEGASUS_BOOTS, POWER_BRACELET)) # boots bonk - bird_key.connect(bird_cave, AND(FEATHER, PEGASUS_BOOTS)) # boots jump above wall, use multiple pit buffers to get across - mountain_bridge_staircase.connect(outside_rooster_house, OR(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across - left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(PEGASUS_BOOTS, FEATHER), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left - left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area + below_right_taltal.connect(raft_game, AND(OR(r.jesus_jump, r.jesus_rooster), r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario) + outside_raft_house.connect(below_right_taltal, AND(r.super_jump, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect. Boots to get out of wall after landing + bridge_seashell.connect(outside_rooster_house, AND(OR(r.hookshot_spam_pit, r.boots_bonk_pit), POWER_BRACELET)) # boots bonk or hookshot spam over the pit to get to the rock + bird_key.connect(bird_cave, AND(r.boots_jump, r.pit_buffer)) # boots jump above wall, use multiple pit buffers to get across + right_taltal_connector2.connect(right_taltal_connector3, r.pit_buffer_itemless, one_way=True) # 2 separate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen + mountain_bridge_staircase.connect(outside_rooster_house, r.pit_buffer_boots) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across + left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(r.boots_jump, r.pit_buffer), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(r.boots_roosterhop, r.super_jump_rooster)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area + + windfish.connect(nightmare, AND(SWORD, OR(BOOMERANG, BOW, BOMB, COUNT(SWORD, 2), AND(OCARINA, OR(SONG1, SONG3))))) # sword quick kill blob, can kill dethl with bombs or sword beams, and can use ocarina to freeze one of ganon's bats to skip dethl eye phase self.start = start_house self.egg = windfish_egg diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py index a8e57327e78b..fa01627a15c3 100644 --- a/worlds/ladx/LADXR/logic/requirements.py +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -254,18 +254,62 @@ def isConsumable(item) -> bool: class RequirementsSettings: def __init__(self, options): self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG) + self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG) - self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # switches, hinox, shrouded stalfos + self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos + self.hit_switch = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hit switches in dungeons self.attack_hookshot_powder = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT, MAGIC_POWDER) # zols, keese, moldorm self.attack_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # ? self.attack_hookshot_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # vire self.attack_no_boomerang = OR(SWORD, BOMB, BOW, MAGIC_ROD, HOOKSHOT) # teleporting owls self.attack_skeleton = OR(SWORD, BOMB, BOW, BOOMERANG, HOOKSHOT) # cannot kill skeletons with the fire rod + self.attack_gibdos = OR(SWORD, BOMB, BOW, BOOMERANG, AND(MAGIC_ROD, HOOKSHOT)) # gibdos are only stunned with hookshot, but can be burnt to jumping stalfos first with magic rod + self.attack_pols_voice = OR(BOMB, MAGIC_ROD, AND(OCARINA, SONG1)) # BOW works, but isn't as reliable as it needs 4 arrows. + self.attack_wizrobe = OR(BOMB, MAGIC_ROD) # BOW works, but isn't as reliable as it needs 4 arrows. + self.stun_wizrobe = OR(BOOMERANG, MAGIC_POWDER, HOOKSHOT) self.rear_attack = OR(SWORD, BOMB) # mimic self.rear_attack_range = OR(MAGIC_ROD, BOW) # mimic self.fire = OR(MAGIC_POWDER, MAGIC_ROD) # torches self.push_hardhat = OR(SHIELD, SWORD, HOOKSHOT, BOOMERANG) - self.shuffled_magnifier = TRADING_ITEM_MAGNIFYING_GLASS + self.shuffled_magnifier = TRADING_ITEM_MAGNIFYING_GLASS # overwritten if vanilla trade items + + self.throw_pot = POWER_BRACELET # grab pots to kill enemies + self.throw_enemy = POWER_BRACELET # grab stunned enemies to kill enemies + self.tight_jump = FEATHER # jumps that are possible but are tight to make it across + self.super_jump = AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)) # standard superjump for glitch logic + self.super_jump_boots = AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD)) # boots dash into wall for unclipped superjump + self.super_jump_feather = FEATHER # using only feather to align and jump off walls + self.super_jump_sword = AND(FEATHER, SWORD) # unclipped superjumps + self.super_jump_rooster = AND(ROOSTER, OR(SWORD, BOW, MAGIC_ROD)) # use rooster instead of feather to superjump off walls (only where rooster is allowed to be used) + self.shaq_jump = FEATHER # use interactable objects (keyblocks / pushable blocks) + self.boots_superhop = AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW)) # dash into walls, pause, unpause and use weapon + hold direction away from wall. Only works in peg rooms + self.boots_roosterhop = AND(PEGASUS_BOOTS, ROOSTER) # dash towards a wall, pick up the rooster and throw it away from the wall before hitting the wall to get a superjump + self.jesus_jump = FEATHER # pause on the frame of hitting liquid (water / lava) to be able to jump again on unpause + self.jesus_buffer = PEGASUS_BOOTS # use a boots bonk to get on top of liquid (water / lava), then use buffers to get into positions + self.damage_boost_special = options.hardmode == "none" # use damage to cross pits / get through forced barriers without needing an enemy that can be eaten by bowwow + self.damage_boost = (options.bowwow == "normal") & (options.hardmode == "none") # Use damage to cross pits / get through forced barriers + self.sideways_block_push = True # wall clip pushable block, get against the edge and push block to move it sideways + self.wall_clip = True # push into corners to get further into walls, to avoid collision with enemies along path (see swamp flowers for example) or just getting a better position for jumps + self.pit_buffer_itemless = True # walk on top of pits and buffer down + self.pit_buffer = FEATHER # jump on top of pits and buffer to cross vertical gaps + self.pit_buffer_boots = OR(PEGASUS_BOOTS, FEATHER) # use boots or feather to cross gaps + self.boots_jump = AND(PEGASUS_BOOTS, FEATHER) # use boots jumps to cross 4 gap spots or other hard to reach spots + self.boots_bonk = PEGASUS_BOOTS # bonk against walls in 2d sections to get to higher places (no pits involved usually) + self.boots_bonk_pit = PEGASUS_BOOTS # use boots bonks to cross 1 tile gaps + self.boots_bonk_2d_spikepit = AND(PEGASUS_BOOTS, "MEDICINE2") # use iframes from medicine to get a boots dash going in 2d spike pits (kanalet secret passage, d3 2d section to boss) + self.boots_bonk_2d_hell = PEGASUS_BOOTS # seperate boots bonks from hell logic which are harder? + self.boots_dash_2d = PEGASUS_BOOTS # use boots to dash over 1 tile gaps in 2d sections + self.hookshot_spam_pit = HOOKSHOT # use hookshot with spam to cross 1 tile gaps + self.hookshot_clip = AND(HOOKSHOT, options.superweapons == False) # use hookshot at specific angles to hookshot past blocks (see forest north log cave, dream shrine entrance for example) + self.hookshot_clip_block = HOOKSHOT # use hookshot spam with enemies to clip through entire blocks (d5 room before gohma, d2 pots room before boss) + self.hookshot_over_pit = HOOKSHOT # use hookshot while over a pit to reach certain areas (see d3 vacuum room, d5 north of crossroads for example) + self.hookshot_jump = AND(HOOKSHOT, FEATHER) # while over pits, on the first frame after the hookshot is retracted you can input a jump to cross big pit gaps + self.bookshot = AND(FEATHER, HOOKSHOT) # use feather on A, hookshot on B on the same frame to get a speedy hookshot that can be used to clip past blocks + self.bomb_trigger = BOMB # drop two bombs at the same time to trigger cutscenes or pickup items (can use pits, or screen transitions + self.shield_bump = SHIELD # use shield to knock back enemies or knock off enemies when used in combination with superjumps + self.text_clip = False & options.nagmessages # trigger a text box on keyblock or rock or obstacle while holding diagonal to clip into the side. Removed from logic for now + self.jesus_rooster = AND(ROOSTER, options.hardmode != "oracle") # when transitioning on top of water, buffer the rooster out of sq menu to spawn it. Then do an unbuffered pickup of the rooster as soon as you spawn again to pick it up + self.zoomerang = AND(PEGASUS_BOOTS, FEATHER, BOOMERANG) # after starting a boots dash, buffer boomerang (on b), feather and the direction you're dashing in to get boosted in certain directions self.boss_requirements = [ SWORD, # D1 boss @@ -283,7 +327,7 @@ def __init__(self, options): "HINOX": self.attack_hookshot, "DODONGO": BOMB, "CUE_BALL": SWORD, - "GHOMA": OR(BOW, HOOKSHOT), + "GHOMA": OR(BOW, HOOKSHOT, MAGIC_ROD, BOOMERANG), "SMASHER": POWER_BRACELET, "GRIM_CREEPER": self.attack_hookshot_no_bomb, "BLAINO": SWORD, @@ -295,10 +339,14 @@ def __init__(self, options): # Adjust for options if not options.tradequest: - self.shuffled_magnifier = True - if options.bowwow != 'normal': + self.shuffled_magnifier = True # completing trade quest not required + if options.hardmode == "ohko": + self.miniboss_requirements["ROLLING_BONES"] = OR(BOW, MAGIC_ROD, BOOMERANG, AND(FEATHER, self.attack_hookshot)) # should not deal with roller damage + if options.bowwow != "normal": # We cheat in bowwow mode, we pretend we have the sword, as bowwow can pretty much do all what the sword ca$ # Except for taking out bushes (and crystal pillars are removed) self.bush.remove(SWORD) + self.pit_bush.remove(SWORD) + self.hit_switch.remove(SWORD) if options.logic == "casual": # In casual mode, remove the more complex kill methods self.bush.remove(MAGIC_POWDER) @@ -308,14 +356,18 @@ def __init__(self, options): self.attack_hookshot_powder.remove(BOMB) self.attack_no_boomerang.remove(BOMB) self.attack_skeleton.remove(BOMB) - if options.logic == "hard": + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + self.boss_requirements[1] = AND(OR(SWORD, MAGIC_ROD, BOMB), POWER_BRACELET) # bombs + bracelet genie self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish self.boss_requirements[6] = OR(MAGIC_ROD, AND(BOMB, BOW), COUNT(SWORD, 2), AND(OR(SWORD, HOOKSHOT, BOW), SHIELD)) # evil eagle 3 cycle magic rod / bomb arrows / l2 sword, and bow kill - if options.logic == "glitched": - self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.attack_pols_voice = OR(BOMB, MAGIC_ROD, AND(OCARINA, SONG1), AND(self.stun_wizrobe, self.throw_enemy, BOW)) # wizrobe stun has same req as pols voice stun + self.attack_wizrobe = OR(BOMB, MAGIC_ROD, AND(self.stun_wizrobe, self.throw_enemy, BOW)) + + if options.logic == 'glitched' or options.logic == 'hell': self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs + if options.logic == "hell": - self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish - self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs self.boss_requirements[7] = OR(MAGIC_ROD, COUNT(SWORD, 2)) # hot head sword beams + self.miniboss_requirements["GHOMA"] = OR(BOW, HOOKSHOT, MAGIC_ROD, BOOMERANG, AND(OCARINA, BOMB, OR(SONG1, SONG3))) # use bombs to kill gohma, with ocarina to get good timings self.miniboss_requirements["GIANT_BUZZ_BLOB"] = OR(MAGIC_POWDER, COUNT(SWORD,2)) # use sword beams to damage buzz blob diff --git a/worlds/ladx/test/TestDungeonLogic.py b/worlds/ladx/test/TestDungeonLogic.py index b9b9672b9b16..3202afa95bc1 100644 --- a/worlds/ladx/test/TestDungeonLogic.py +++ b/worlds/ladx/test/TestDungeonLogic.py @@ -10,7 +10,7 @@ class TestD6(LADXTestBase): def test_keylogic(self): keys = self.get_items_by_name(ItemName.KEY6) - self.collect_by_name([ItemName.FACE_KEY, ItemName.HOOKSHOT, ItemName.POWER_BRACELET, ItemName.BOMB, ItemName.FEATHER, ItemName.FLIPPERS]) + self.collect_by_name([ItemName.FACE_KEY, ItemName.HOOKSHOT, ItemName.POWER_BRACELET, ItemName.BOMB, ItemName.PEGASUS_BOOTS, ItemName.FEATHER, ItemName.FLIPPERS]) # Can reach an un-keylocked item in the dungeon self.assertTrue(self.can_reach_location("L2 Bracelet Chest (Face Shrine)")) @@ -18,18 +18,18 @@ def test_keylogic(self): location_1 = "Tile Room Key (Face Shrine)" location_2 = "Top Right Horse Heads Chest (Face Shrine)" location_3 = "Pot Locked Chest (Face Shrine)" - self.assertFalse(self.can_reach_location(location_1)) - self.assertFalse(self.can_reach_location(location_2)) - self.assertFalse(self.can_reach_location(location_3)) + self.assertFalse(self.can_reach_location(location_1), "Tile Room Key, 0 keys") + self.assertFalse(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 0 keys") + self.assertFalse(self.can_reach_location(location_3), "Pot Locked Chest, 0 keys") self.collect(keys[0]) - self.assertTrue(self.can_reach_location(location_1)) - self.assertFalse(self.can_reach_location(location_2)) - self.assertFalse(self.can_reach_location(location_3)) + self.assertTrue(self.can_reach_location(location_1), "Tile Room Key, 1 key") + self.assertFalse(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 1 key") + self.assertFalse(self.can_reach_location(location_3), "Pot Locked Chest, 1 key") self.collect(keys[1]) - self.assertTrue(self.can_reach_location(location_1)) - self.assertTrue(self.can_reach_location(location_2)) - self.assertFalse(self.can_reach_location(location_3)) + self.assertTrue(self.can_reach_location(location_1), "Tile Room Key, 2 keys") + self.assertTrue(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 2 keys") + self.assertFalse(self.can_reach_location(location_3), "Pot Locked Chest, 2 keys") self.collect(keys[2]) - self.assertTrue(self.can_reach_location(location_1)) - self.assertTrue(self.can_reach_location(location_2)) - self.assertTrue(self.can_reach_location(location_3)) + self.assertTrue(self.can_reach_location(location_1), "Tile Room Key, 3 keys") + self.assertTrue(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 3 keys") + self.assertTrue(self.can_reach_location(location_3), "Pot Locked Chest, 3 keys") From f4b926ebbe491dc9a064b5d17bdd95051a5c6496 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 5 Dec 2024 07:33:21 -0800 Subject: [PATCH 092/112] Pokemon Emerald: Exclude sacred ash post champion (#4207) * Pokemon Emerald: Exclude sacred ash post champion * Pokemon Emerald: Remove .value from toggle option check --- worlds/pokemon_emerald/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 040b89b1af51..7b62b9ef73b1 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -297,6 +297,12 @@ def exclude_locations(location_names: List[str]): "Safari Zone SE - Hidden Item in South Grass 2", "Safari Zone SE - Item in Grass", ]) + + # Sacred ash is on Navel Rock, which is locked behind the event tickets + if not self.options.event_tickets: + exclude_locations([ + "Navel Rock Top - Hidden Item Sacred Ash", + ]) elif self.options.goal == Goal.option_steven: exclude_locations([ "Meteor Falls 1F - Rival Steven", From ced93022b67532e8172fd217a7d3a7bd205b8177 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Fri, 6 Dec 2024 01:15:26 -0500 Subject: [PATCH 093/112] Adventure: Remove unused variables (#4301) * Remove unused variables * Provide old parameters to comment --- worlds/adventure/Locations.py | 2 -- worlds/adventure/Regions.py | 6 +++--- worlds/adventure/Rom.py | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/worlds/adventure/Locations.py b/worlds/adventure/Locations.py index 27e504684cbf..ddaa266e5b74 100644 --- a/worlds/adventure/Locations.py +++ b/worlds/adventure/Locations.py @@ -47,8 +47,6 @@ def __init__(self, region, name, location_id, world_positions: [WorldPosition] = self.local_item: int = None def get_random_position(self, random): - x: int = None - y: int = None if self.world_positions is None or len(self.world_positions) == 0: if self.room_id is None: return None diff --git a/worlds/adventure/Regions.py b/worlds/adventure/Regions.py index e72806ca454f..4e4dd1e7baa1 100644 --- a/worlds/adventure/Regions.py +++ b/worlds/adventure/Regions.py @@ -76,10 +76,9 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player multiworld.regions.append(credits_room_far_side) dragon_slay_check = options.dragon_slay_check.value - priority_locations = determine_priority_locations(multiworld, dragon_slay_check) + priority_locations = determine_priority_locations() for name, location_data in location_table.items(): - require_sword = False if location_data.region == "Varies": if location_data.name == "Slay Yorgle": if not dragon_slay_check: @@ -154,6 +153,7 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player # Placeholder for adding sets of priority locations at generation, possibly as an option in the future -def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}: +# def determine_priority_locations(multiworld: MultiWorld, dragon_slay_check: bool) -> {}: +def determine_priority_locations() -> {}: priority_locations = {} return priority_locations diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py index ca64e569716a..643f7a6c766c 100644 --- a/worlds/adventure/Rom.py +++ b/worlds/adventure/Rom.py @@ -86,9 +86,7 @@ class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister): # locations: [], autocollect: [], seed_name: bytes, def __init__(self, *args: Any, **kwargs: Any) -> None: - patch_only = True if "autocollect" in kwargs: - patch_only = False self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y) for loc in kwargs["locations"]] From c9625e1b35c36866ee727128fe73808580f37145 Mon Sep 17 00:00:00 2001 From: LeonarthCG <33758848+LeonarthCG@users.noreply.github.com> Date: Sat, 7 Dec 2024 11:29:27 +0100 Subject: [PATCH 094/112] Saving Princess: implement new game (#3238) * Saving Princess: initial commit * settings -> options Co-authored-by: Scipio Wright * settings -> options Co-authored-by: Scipio Wright * replace RegionData class with List[str] RegionData was only wrapping a List[str], so we can directly use List[str] * world: MultiWorld -> multiworld: MultiWorld * use world's random instead of multiworld's * use state's has_any and has_all where applicable * remove unused StartInventory import * reorder PerGameCommonOptions * fix relative AutoWorld import Co-authored-by: Scipio Wright * clean up double spaces * local commands -> Local Commands Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * remove redundant which items section Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * game info rework * clean up item count redundancy * add game to readme and codeowners * fix get_region_entrance return type * world.multiworld.get -> world.get * add more events added events for the boss kills that open the gate, as well as for system power being restored these only apply if expanded pool is not selected * add client/autoupdater to launcher * reorder commands in game info * update docs with automated installation info * add quick links to doc * Update setup_en.md * remove standalone saving princess client * doc fixes * code improvements and redundant default removal as suggested by @Exempt-Medic this includes the removal of events from the item/location name to id, as well as checking for the player name being ASCII * add option to change launch coammnd the LaunchCommand option is filled to either the executable or wine with the necessary arguments based on Utils.is_windows * simplify valid install check * mod installer improvements now deletes possible existing files before installing the mod * add option groups and presets * add required client version * update docs about cheat items pop-ups items sent directly by the server (such as with starting inventory) now have pop-ups just like any other item * add Steam Input issue to faq * Saving Princess: BRAINOS requires all weapons * Saving Princess: Download dll and patch together Previously, gm-apclientpp.dll was downloaded from its own repo With this update, the dll is instead extracted from the same zip as the game's patch * Saving Princess: Add URI launch support * Saving Princess: goal also requires all weapons given it's past brainos * Saving Princess: update docs automatic connection support was added, docs now reflect this * Saving Princess: extend([item]) -> append(item) * Saving Princess: automatic connection validation also parses the slot, password and host:port into parameters for the game * Saving Princess: change subprocess .run to .Popen This keeps the game from freezing the launcher while it is running --------- Co-authored-by: Scipio Wright Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/saving_princess/Client.py | 258 ++++++++++++++++++ worlds/saving_princess/Constants.py | 97 +++++++ worlds/saving_princess/Items.py | 98 +++++++ worlds/saving_princess/Locations.py | 82 ++++++ worlds/saving_princess/Options.py | 183 +++++++++++++ worlds/saving_princess/Regions.py | 110 ++++++++ worlds/saving_princess/Rules.py | 132 +++++++++ worlds/saving_princess/__init__.py | 174 ++++++++++++ .../docs/en_Saving Princess.md | 55 ++++ worlds/saving_princess/docs/setup_en.md | 148 ++++++++++ 12 files changed, 1341 insertions(+) create mode 100644 worlds/saving_princess/Client.py create mode 100644 worlds/saving_princess/Constants.py create mode 100644 worlds/saving_princess/Items.py create mode 100644 worlds/saving_princess/Locations.py create mode 100644 worlds/saving_princess/Options.py create mode 100644 worlds/saving_princess/Regions.py create mode 100644 worlds/saving_princess/Rules.py create mode 100644 worlds/saving_princess/__init__.py create mode 100644 worlds/saving_princess/docs/en_Saving Princess.md create mode 100644 worlds/saving_princess/docs/setup_en.md diff --git a/README.md b/README.md index 2cc3c18aa09d..21a6faaa2698 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Currently, the following games are supported: * Mega Man 2 * Yacht Dice * Faxanadu +* Saving Princess 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 diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 64a1362bf380..1aec57fc90f6 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -142,6 +142,9 @@ # Risk of Rain 2 /worlds/ror2/ @kindasneaki +# Saving Princess +/worlds/saving_princess/ @LeonarthCG + # Shivers /worlds/shivers/ @GodlFire diff --git a/worlds/saving_princess/Client.py b/worlds/saving_princess/Client.py new file mode 100644 index 000000000000..29a97bb667c0 --- /dev/null +++ b/worlds/saving_princess/Client.py @@ -0,0 +1,258 @@ +import argparse +import zipfile +from io import BytesIO + +import bsdiff4 +from datetime import datetime +import hashlib +import json +import logging +import os +import requests +import secrets +import shutil +import subprocess +from tkinter import messagebox +from typing import Any, Dict, Set +import urllib +import urllib.parse + +import Utils +from .Constants import * +from . import SavingPrincessWorld + +files_to_clean: Set[str] = { + "D3DX9_43.dll", + "data.win", + "m_boss.ogg", + "m_brainos.ogg", + "m_coldarea.ogg", + "m_escape.ogg", + "m_hotarea.ogg", + "m_hsis_dark.ogg", + "m_hsis_power.ogg", + "m_introarea.ogg", + "m_malakhov.ogg", + "m_miniboss.ogg", + "m_ninja.ogg", + "m_purple.ogg", + "m_space_idle.ogg", + "m_stonearea.ogg", + "m_swamp.ogg", + "m_zzz.ogg", + "options.ini", + "Saving Princess v0_8.exe", + "splash.png", + "gm-apclientpp.dll", + "LICENSE", + "original_data.win", + "versions.json", +} + +file_hashes: Dict[str, str] = { + "D3DX9_43.dll": "86e39e9161c3d930d93822f1563c280d", + "Saving Princess v0_8.exe": "cc3ad10c782e115d93c5b9fbc5675eaf", + "original_data.win": "f97b80204bd9ae535faa5a8d1e5eb6ca", +} + + +class UrlResponse: + def __init__(self, response_code: int, data: Any): + self.response_code = response_code + self.data = data + + +def get_date(target_asset: str) -> str: + """Provided the name of an asset, fetches its update date""" + try: + with open("versions.json", "r") as versions_json: + return json.load(versions_json)[target_asset] + except (FileNotFoundError, KeyError, json.decoder.JSONDecodeError): + return "2000-01-01T00:00:00Z" # arbitrary old date + + +def set_date(target_asset: str, date: str) -> None: + """Provided the name of an asset and a date, sets it update date""" + try: + with open("versions.json", "r") as versions_json: + versions = json.load(versions_json) + versions[target_asset] = date + except (FileNotFoundError, KeyError, json.decoder.JSONDecodeError): + versions = {target_asset: date} + with open("versions.json", "w") as versions_json: + json.dump(versions, versions_json) + + +def get_timestamp(date: str) -> float: + """Parses a GitHub REST API date into a timestamp""" + return datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").timestamp() + + +def send_request(request_url: str) -> UrlResponse: + """Fetches status code and json response from given url""" + response = requests.get(request_url) + if response.status_code == 200: # success + try: + data = response.json() + except requests.exceptions.JSONDecodeError: + raise RuntimeError(f"Unable to fetch data. (status code {response.status_code}).") + else: + data = {} + return UrlResponse(response.status_code, data) + + +def update(target_asset: str, url: str) -> bool: + """ + Returns True if the data was fetched and installed + (or it was already on the latest version, or the user refused the update) + Returns False if rate limit was exceeded + """ + try: + logging.info(f"Checking for {target_asset} updates.") + response = send_request(url) + if response.response_code == 403: # rate limit exceeded + return False + assets = response.data[0]["assets"] + for asset in assets: + if target_asset in asset["name"]: + newest_date: str = asset["updated_at"] + release_url: str = asset["browser_download_url"] + break + else: + raise RuntimeError(f"Failed to locate {target_asset} amongst the assets.") + except (KeyError, IndexError, TypeError, RuntimeError): + update_error = f"Failed to fetch latest {target_asset}." + messagebox.showerror("Failure", update_error) + raise RuntimeError(update_error) + try: + update_available = get_timestamp(newest_date) > get_timestamp(get_date(target_asset)) + if update_available and messagebox.askyesnocancel(f"New {target_asset}", + "Would you like to install the new version now?"): + # unzip and patch + with urllib.request.urlopen(release_url) as download: + with zipfile.ZipFile(BytesIO(download.read())) as zf: + zf.extractall() + patch_game() + set_date(target_asset, newest_date) + except (ValueError, RuntimeError, urllib.error.HTTPError): + update_error = f"Failed to apply update." + messagebox.showerror("Failure", update_error) + raise RuntimeError(update_error) + return True + + +def patch_game() -> None: + """Applies the patch to data.win""" + logging.info("Proceeding to patch.") + with open(PATCH_NAME, "rb") as patch: + with open("original_data.win", "rb") as data: + patched_data = bsdiff4.patch(data.read(), patch.read()) + with open("data.win", "wb") as data: + data.write(patched_data) + logging.info("Done!") + + +def is_install_valid() -> bool: + """Checks that the mandatory files that we cannot replace do exist in the current folder""" + for file_name, expected_hash in file_hashes.items(): + if not os.path.exists(file_name): + return False + with open(file_name, "rb") as clean: + current_hash = hashlib.md5(clean.read()).hexdigest() + if not secrets.compare_digest(current_hash, expected_hash): + return False + return True + + +def install() -> None: + """Extracts all the game files into the mod installation folder""" + logging.info("Mod installation missing or corrupted, proceeding to reinstall.") + # get the cab file and extract it into the installation folder + with open(SavingPrincessWorld.settings.exe_path, "rb") as exe: + # find the cab header + logging.info("Looking for cab archive inside exe.") + cab_found: bool = False + while not cab_found: + cab_found = exe.read(1) == b'M' and exe.read(1) == b'S' and exe.read(1) == b'C' and exe.read(1) == b'F' + exe.read(4) # skip reserved1, always 0 + cab_size: int = int.from_bytes(exe.read(4), "little") # read size in bytes + exe.seek(-12, 1) # move the cursor back to the start of the cab file + logging.info(f"Archive found at offset {hex(exe.seek(0, 1))}, size: {hex(cab_size)}.") + logging.info("Extracting cab archive from exe.") + with open("saving_princess.cab", "wb") as cab: + cab.write(exe.read(cab_size)) + + # clean up files from previous installations + for file_name in files_to_clean: + if os.path.exists(file_name): + os.remove(file_name) + + logging.info("Extracting files from cab archive.") + if Utils.is_windows: + subprocess.run(["Extrac32", "/Y", "/E", "saving_princess.cab"]) + else: + if shutil.which("wine") is not None: + subprocess.run(["wine", "Extrac32", "/Y", "/E", "saving_princess.cab"]) + elif shutil.which("7z") is not None: + subprocess.run(["7z", "e", "saving_princess.cab"]) + else: + error = "Could not find neither wine nor 7z.\n\nPlease install either the wine or the p7zip package." + messagebox.showerror("Missing package!", f"Error: {error}") + raise RuntimeError(error) + os.remove("saving_princess.cab") # delete the cab file + + shutil.copyfile("data.win", "original_data.win") # and make a copy of data.win + logging.info("Done!") + + +def launch(*args: str) -> Any: + """Check args, then the mod installation, then launch the game""" + name: str = "" + password: str = "" + server: str = "" + if args: + parser = argparse.ArgumentParser(description=f"{GAME_NAME} Client Launcher") + parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") + args = parser.parse_args(args) + + # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost + if args.url: + url = urllib.parse.urlparse(args.url) + if url.scheme == "archipelago": + server = f'--server="{url.hostname}:{url.port}"' + if url.username: + name = f'--name="{urllib.parse.unquote(url.username)}"' + if url.password: + password = f'--password="{urllib.parse.unquote(url.password)}"' + else: + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + + Utils.init_logging(CLIENT_NAME, exception_logger="Client") + + os.chdir(SavingPrincessWorld.settings.install_folder) + + # check that the mod installation is valid + if not is_install_valid(): + if messagebox.askyesnocancel(f"Mod installation missing or corrupted!", + "Would you like to reinstall now?"): + install() + # if there is no mod installation, and we are not installing it, then there isn't much to do + else: + return + + # check for updates + if not update(DOWNLOAD_NAME, DOWNLOAD_URL): + messagebox.showinfo("Rate limit exceeded", + "GitHub REST API limit exceeded, could not check for updates.\n\n" + "This will not prevent the game from being played if it was already playable.") + + # and try to launch the game + if SavingPrincessWorld.settings.launch_game: + logging.info("Launching game.") + try: + subprocess.Popen(f"{SavingPrincessWorld.settings.launch_command} {name} {password} {server}") + except FileNotFoundError: + error = ("Could not run the game!\n\n" + "Please check that launch_command in options.yaml or host.yaml is set up correctly.") + messagebox.showerror("Command error!", f"Error: {error}") + raise RuntimeError(error) diff --git a/worlds/saving_princess/Constants.py b/worlds/saving_princess/Constants.py new file mode 100644 index 000000000000..0dde18779727 --- /dev/null +++ b/worlds/saving_princess/Constants.py @@ -0,0 +1,97 @@ +GAME_NAME: str = "Saving Princess" +BASE_ID: int = 0x53565052494E # SVPRIN + +# client installation data +CLIENT_NAME = f"{GAME_NAME.replace(' ', '')}Client" +GAME_HASH = "35a111d0149fae1f04b7b3fea42c5319" +PATCH_NAME = "saving_princess_basepatch.bsdiff4" +DOWNLOAD_NAME = "saving_princess_archipelago.zip" +DOWNLOAD_URL = "https://api.github.com/repos/LeonarthCG/saving-princess-archipelago/releases" + +# item names +ITEM_WEAPON_CHARGE: str = "Powered Blaster" +ITEM_WEAPON_FIRE: str = "Flamethrower" +ITEM_WEAPON_ICE: str = "Ice Spreadshot" +ITEM_WEAPON_VOLT: str = "Volt Laser" +ITEM_MAX_HEALTH: str = "Life Extension" +ITEM_MAX_AMMO: str = "Clip Extension" +ITEM_RELOAD_SPEED: str = "Faster Reload" +ITEM_SPECIAL_AMMO: str = "Special Extension" +ITEM_JACKET: str = "Jacket" + +EP_ITEM_GUARD_GONE: str = "Cave Key" +EP_ITEM_CLIFF_GONE: str = "Volcanic Key" +EP_ITEM_ACE_GONE: str = "Arctic Key" +EP_ITEM_SNAKE_GONE: str = "Swamp Key" +EP_ITEM_POWER_ON: str = "System Power" + +FILLER_ITEM_HEAL: str = "Full Heal" +FILLER_ITEM_QUICK_FIRE: str = "Quick-fire Mode" +FILLER_ITEM_ACTIVE_CAMO: str = "Active Camouflage" + +TRAP_ITEM_ICE: str = "Ice Trap" +TRAP_ITEM_SHAKES: str = "Shake Trap" +TRAP_ITEM_NINJA: str = "Ninja Trap" + +EVENT_ITEM_GUARD_GONE: str = "Guard neutralized" +EVENT_ITEM_CLIFF_GONE: str = "Cliff neutralized" +EVENT_ITEM_ACE_GONE: str = "Ace neutralized" +EVENT_ITEM_SNAKE_GONE: str = "Snake neutralized" +EVENT_ITEM_POWER_ON: str = "Power restored" +EVENT_ITEM_VICTORY: str = "PRINCESS" + +# location names, EP stands for Expanded Pool +LOCATION_CAVE_AMMO: str = "Cave: After Wallboss" +LOCATION_CAVE_RELOAD: str = "Cave: Balcony" +LOCATION_CAVE_HEALTH: str = "Cave: Spike pit" +LOCATION_CAVE_WEAPON: str = "Cave: Powered Blaster chest" +LOCATION_VOLCANIC_RELOAD: str = "Volcanic: Hot coals" +LOCATION_VOLCANIC_HEALTH: str = "Volcanic: Under bridge" +LOCATION_VOLCANIC_AMMO: str = "Volcanic: Behind wall" +LOCATION_VOLCANIC_WEAPON: str = "Volcanic: Flamethrower chest" +LOCATION_ARCTIC_AMMO: str = "Arctic: Before pipes" +LOCATION_ARCTIC_RELOAD: str = "Arctic: After Guard" +LOCATION_ARCTIC_HEALTH: str = "Arctic: Under snow" +LOCATION_ARCTIC_WEAPON: str = "Arctic: Ice Spreadshot chest" +LOCATION_JACKET: str = "Arctic: Jacket chest" +LOCATION_HUB_AMMO: str = "Hub: Hidden near Arctic" +LOCATION_HUB_HEALTH: str = "Hub: Hidden near Cave" +LOCATION_HUB_RELOAD: str = "Hub: Hidden near Swamp" +LOCATION_SWAMP_AMMO: str = "Swamp: Bramble room" +LOCATION_SWAMP_HEALTH: str = "Swamp: Down the chimney" +LOCATION_SWAMP_RELOAD: str = "Swamp: Wall maze" +LOCATION_SWAMP_SPECIAL: str = "Swamp: Special Extension chest" +LOCATION_ELECTRICAL_RELOAD: str = "Electrical: Near generator" +LOCATION_ELECTRICAL_HEALTH: str = "Electrical: Behind wall" +LOCATION_ELECTRICAL_AMMO: str = "Electrical: Before Malakhov" +LOCATION_ELECTRICAL_WEAPON: str = "Electrical: Volt Laser chest" + +EP_LOCATION_CAVE_MINIBOSS: str = "Cave: Wallboss (Boss)" +EP_LOCATION_CAVE_BOSS: str = "Cave: Guard (Boss)" +EP_LOCATION_VOLCANIC_BOSS: str = "Volcanic: Cliff (Boss)" +EP_LOCATION_ARCTIC_BOSS: str = "Arctic: Ace (Boss)" +EP_LOCATION_HUB_CONSOLE: str = "Hub: Console login" +EP_LOCATION_HUB_NINJA_SCARE: str = "Hub: Ninja scare (Boss?)" +EP_LOCATION_SWAMP_BOSS: str = "Swamp: Snake (Boss)" +EP_LOCATION_ELEVATOR_NINJA_FIGHT: str = "Elevator: Ninja (Boss)" +EP_LOCATION_ELECTRICAL_EXTRA: str = "Electrical: Tesla orb" +EP_LOCATION_ELECTRICAL_MINIBOSS: str = "Electrical: Generator (Boss)" +EP_LOCATION_ELECTRICAL_BOSS: str = "Electrical: Malakhov (Boss)" +EP_LOCATION_ELECTRICAL_FINAL_BOSS: str = "Electrical: BRAINOS (Boss)" + +EVENT_LOCATION_GUARD_GONE: str = "Cave status" +EVENT_LOCATION_CLIFF_GONE: str = "Volcanic status" +EVENT_LOCATION_ACE_GONE: str = "Arctic status" +EVENT_LOCATION_SNAKE_GONE: str = "Swamp status" +EVENT_LOCATION_POWER_ON: str = "Generator status" +EVENT_LOCATION_VICTORY: str = "Mission objective" + +# region names +REGION_MENU: str = "Menu" +REGION_CAVE: str = "Cave" +REGION_VOLCANIC: str = "Volcanic" +REGION_ARCTIC: str = "Arctic" +REGION_HUB: str = "Hub" +REGION_SWAMP: str = "Swamp" +REGION_ELECTRICAL: str = "Electrical" +REGION_ELECTRICAL_POWERED: str = "Electrical (Power On)" diff --git a/worlds/saving_princess/Items.py b/worlds/saving_princess/Items.py new file mode 100644 index 000000000000..4c1fe78a9c72 --- /dev/null +++ b/worlds/saving_princess/Items.py @@ -0,0 +1,98 @@ +from typing import Optional, Dict, Tuple + +from BaseClasses import Item, ItemClassification as ItemClass + +from .Constants import * + + +class SavingPrincessItem(Item): + game: str = GAME_NAME + + +class ItemData: + item_class: ItemClass + code: Optional[int] + count: int # Number of copies for the item that will be made of class item_class + count_extra: int # Number of extra copies for the item that will be made as useful + + def __init__(self, item_class: ItemClass, code: Optional[int] = None, count: int = 1, count_extra: int = 0): + self.item_class = item_class + + self.code = code + if code is not None: + self.code += BASE_ID + + # if this is filler, a trap or an event, ignore the count + if self.item_class == ItemClass.filler or self.item_class == ItemClass.trap or code is None: + self.count = 0 + self.count_extra = 0 + else: + self.count = count + self.count_extra = count_extra + + def create_item(self, player: int): + return SavingPrincessItem(item_data_names[self], self.item_class, self.code, player) + + +item_dict_weapons: Dict[str, ItemData] = { + ITEM_WEAPON_CHARGE: ItemData(ItemClass.progression, 0), + ITEM_WEAPON_FIRE: ItemData(ItemClass.progression, 1), + ITEM_WEAPON_ICE: ItemData(ItemClass.progression, 2), + ITEM_WEAPON_VOLT: ItemData(ItemClass.progression, 3), +} + +item_dict_upgrades: Dict[str, ItemData] = { + ITEM_MAX_HEALTH: ItemData(ItemClass.progression, 4, 2, 4), + ITEM_MAX_AMMO: ItemData(ItemClass.progression, 5, 2, 4), + ITEM_RELOAD_SPEED: ItemData(ItemClass.progression, 6, 4, 2), + ITEM_SPECIAL_AMMO: ItemData(ItemClass.useful, 7), +} + +item_dict_base: Dict[str, ItemData] = { + **item_dict_weapons, + **item_dict_upgrades, + ITEM_JACKET: ItemData(ItemClass.useful, 8), +} + +item_dict_keys: Dict[str, ItemData] = { + EP_ITEM_GUARD_GONE: ItemData(ItemClass.progression, 9), + EP_ITEM_CLIFF_GONE: ItemData(ItemClass.progression, 10), + EP_ITEM_ACE_GONE: ItemData(ItemClass.progression, 11), + EP_ITEM_SNAKE_GONE: ItemData(ItemClass.progression, 12), +} + +item_dict_expanded: Dict[str, ItemData] = { + **item_dict_base, + **item_dict_keys, + EP_ITEM_POWER_ON: ItemData(ItemClass.progression, 13), +} + +item_dict_filler: Dict[str, ItemData] = { + FILLER_ITEM_HEAL: ItemData(ItemClass.filler, 14), + FILLER_ITEM_QUICK_FIRE: ItemData(ItemClass.filler, 15), + FILLER_ITEM_ACTIVE_CAMO: ItemData(ItemClass.filler, 16), +} + +item_dict_traps: Dict[str, ItemData] = { + TRAP_ITEM_ICE: ItemData(ItemClass.trap, 17), + TRAP_ITEM_SHAKES: ItemData(ItemClass.trap, 18), + TRAP_ITEM_NINJA: ItemData(ItemClass.trap, 19), +} + +item_dict_events: Dict[str, ItemData] = { + EVENT_ITEM_GUARD_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_CLIFF_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_ACE_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_SNAKE_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_POWER_ON: ItemData(ItemClass.progression), + EVENT_ITEM_VICTORY: ItemData(ItemClass.progression), +} + +item_dict: Dict[str, ItemData] = { + **item_dict_expanded, + **item_dict_filler, + **item_dict_traps, + **item_dict_events, +} + +item_data_names: Dict[ItemData, str] = {value: key for key, value in item_dict.items()} diff --git a/worlds/saving_princess/Locations.py b/worlds/saving_princess/Locations.py new file mode 100644 index 000000000000..bc7b0f0d6efd --- /dev/null +++ b/worlds/saving_princess/Locations.py @@ -0,0 +1,82 @@ +from typing import Optional, Dict + +from BaseClasses import Location + +from .Constants import * + + +class SavingPrincessLocation(Location): + game: str = GAME_NAME + + +class LocData: + code: Optional[int] + + def __init__(self, code: Optional[int] = None): + if code is not None: + self.code = code + BASE_ID + else: + self.code = None + + +location_dict_base: Dict[str, LocData] = { + LOCATION_CAVE_AMMO: LocData(0), + LOCATION_CAVE_RELOAD: LocData(1), + LOCATION_CAVE_HEALTH: LocData(2), + LOCATION_CAVE_WEAPON: LocData(3), + LOCATION_VOLCANIC_RELOAD: LocData(4), + LOCATION_VOLCANIC_HEALTH: LocData(5), + LOCATION_VOLCANIC_AMMO: LocData(6), + LOCATION_VOLCANIC_WEAPON: LocData(7), + LOCATION_ARCTIC_AMMO: LocData(8), + LOCATION_ARCTIC_RELOAD: LocData(9), + LOCATION_ARCTIC_HEALTH: LocData(10), + LOCATION_ARCTIC_WEAPON: LocData(11), + LOCATION_JACKET: LocData(12), + LOCATION_HUB_AMMO: LocData(13), + LOCATION_HUB_HEALTH: LocData(14), + LOCATION_HUB_RELOAD: LocData(15), + LOCATION_SWAMP_AMMO: LocData(16), + LOCATION_SWAMP_HEALTH: LocData(17), + LOCATION_SWAMP_RELOAD: LocData(18), + LOCATION_SWAMP_SPECIAL: LocData(19), + LOCATION_ELECTRICAL_RELOAD: LocData(20), + LOCATION_ELECTRICAL_HEALTH: LocData(21), + LOCATION_ELECTRICAL_AMMO: LocData(22), + LOCATION_ELECTRICAL_WEAPON: LocData(23), +} + +location_dict_expanded: Dict[str, LocData] = { + **location_dict_base, + EP_LOCATION_CAVE_MINIBOSS: LocData(24), + EP_LOCATION_CAVE_BOSS: LocData(25), + EP_LOCATION_VOLCANIC_BOSS: LocData(26), + EP_LOCATION_ARCTIC_BOSS: LocData(27), + EP_LOCATION_HUB_CONSOLE: LocData(28), + EP_LOCATION_HUB_NINJA_SCARE: LocData(29), + EP_LOCATION_SWAMP_BOSS: LocData(30), + EP_LOCATION_ELEVATOR_NINJA_FIGHT: LocData(31), + EP_LOCATION_ELECTRICAL_EXTRA: LocData(32), + EP_LOCATION_ELECTRICAL_MINIBOSS: LocData(33), + EP_LOCATION_ELECTRICAL_BOSS: LocData(34), + EP_LOCATION_ELECTRICAL_FINAL_BOSS: LocData(35), +} + +location_dict_event_expanded: Dict[str, LocData] = { + EVENT_LOCATION_VICTORY: LocData(), +} + +# most event locations are only relevant without expanded pool +location_dict_events: Dict[str, LocData] = { + EVENT_LOCATION_GUARD_GONE: LocData(), + EVENT_LOCATION_CLIFF_GONE: LocData(), + EVENT_LOCATION_ACE_GONE: LocData(), + EVENT_LOCATION_SNAKE_GONE: LocData(), + EVENT_LOCATION_POWER_ON: LocData(), + **location_dict_event_expanded, +} + +location_dict: Dict[str, LocData] = { + **location_dict_expanded, + **location_dict_events, +} diff --git a/worlds/saving_princess/Options.py b/worlds/saving_princess/Options.py new file mode 100644 index 000000000000..75135a1d15bb --- /dev/null +++ b/worlds/saving_princess/Options.py @@ -0,0 +1,183 @@ +from dataclasses import dataclass +from typing import Dict, Any + +from Options import PerGameCommonOptions, DeathLink, StartInventoryPool, Choice, DefaultOnToggle, Range, Toggle, \ + OptionGroup + + +class ExpandedPool(DefaultOnToggle): + """ + Determines if places other than chests and special weapons will be locations. + This includes boss fights as well as powering the tesla orb and completing the console login. + In Expanded Pool, system power is instead restored when receiving the System Power item. + Similarly, the final area door will open once the four Key items, one for each main area, have been found. + """ + display_name = "Expanded Item Pool" + + +class InstantSaving(DefaultOnToggle): + """ + When enabled, save points activate with no delay when touched. + This makes saving much faster, at the cost of being unable to pick and choose when to save in order to save warp. + """ + display_name = "Instant Saving" + + +class SprintAvailability(Choice): + """ + Determines under which conditions the debug sprint is made accessible to the player. + To sprint, hold down Ctrl if playing on keyboard, or Left Bumper if on gamepad (remappable). + With Jacket: you will not be able to sprint until after the Jacket item has been found. + """ + display_name = "Sprint Availability" + option_never_available = 0 + option_always_available = 1 + option_available_with_jacket = 2 + default = option_available_with_jacket + + +class CliffWeaponUpgrade(Choice): + """ + Determines which weapon Cliff uses against you, base or upgraded. + This does not change the available strategies all that much. + Vanilla: Cliff adds fire to his grenades if Ace has been defeated. + If playing with the expanded pool, the Arctic Key will trigger the change instead. + """ + display_name = "Cliff Weapon Upgrade" + option_never_upgraded = 0 + option_always_upgraded = 1 + option_vanilla = 2 + default = option_always_upgraded + + +class AceWeaponUpgrade(Choice): + """ + Determines which weapon Ace uses against you, base or upgraded. + Ace with his base weapon is very hard to dodge, the upgraded weapon offers a more balanced experience. + Vanilla: Ace uses ice attacks if Cliff has been defeated. + If playing with the expanded pool, the Volcanic Key will trigger the change instead. + """ + display_name = "Ace Weapon Upgrade" + option_never_upgraded = 0 + option_always_upgraded = 1 + option_vanilla = 2 + default = option_always_upgraded + + +class ScreenShakeIntensity(Range): + """ + Percentage multiplier for screen shake effects. + 0% means the screen will not shake at all. + 100% means the screen shake will be the same as in vanilla. + """ + display_name = "Screen Shake Intensity %" + range_start = 0 + range_end = 100 + default = 50 + + +class IFramesDuration(Range): + """ + Percentage multiplier for Portia's invincibility frames. + 0% means you will have no invincibility frames. + 100% means invincibility frames will be the same as vanilla. + """ + display_name = "IFrame Duration %" + range_start = 0 + range_end = 400 + default = 100 + + +class TrapChance(Range): + """ + Likelihood of a filler item becoming a trap. + """ + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 50 + + +class MusicShuffle(Toggle): + """ + Enables music shuffling. + The title screen song is not shuffled, as it plays before the client connects. + """ + display_name = "Music Shuffle" + + +@dataclass +class SavingPrincessOptions(PerGameCommonOptions): + # generation options + start_inventory_from_pool: StartInventoryPool + expanded_pool: ExpandedPool + trap_chance: TrapChance + # gameplay options + death_link: DeathLink + instant_saving: InstantSaving + sprint_availability: SprintAvailability + cliff_weapon_upgrade: CliffWeaponUpgrade + ace_weapon_upgrade: AceWeaponUpgrade + iframes_duration: IFramesDuration + # aesthetic options + shake_intensity: ScreenShakeIntensity + music_shuffle: MusicShuffle + + +groups = [ + OptionGroup("Generation Options", [ + ExpandedPool, + TrapChance, + ]), + OptionGroup("Gameplay Options", [ + DeathLink, + InstantSaving, + SprintAvailability, + CliffWeaponUpgrade, + AceWeaponUpgrade, + IFramesDuration, + ]), + OptionGroup("Aesthetic Options", [ + ScreenShakeIntensity, + MusicShuffle, + ]), +] + +presets = { + "Vanilla-like": { + "expanded_pool": False, + "trap_chance": 0, + "death_link": False, + "instant_saving": False, + "sprint_availability": SprintAvailability.option_never_available, + "cliff_weapon_upgrade": CliffWeaponUpgrade.option_vanilla, + "ace_weapon_upgrade": AceWeaponUpgrade.option_vanilla, + "iframes_duration": 100, + "shake_intensity": 100, + "music_shuffle": False, + }, + "Easy": { + "expanded_pool": True, + "trap_chance": 0, + "death_link": False, + "instant_saving": True, + "sprint_availability": SprintAvailability.option_always_available, + "cliff_weapon_upgrade": CliffWeaponUpgrade.option_never_upgraded, + "ace_weapon_upgrade": AceWeaponUpgrade.option_always_upgraded, + "iframes_duration": 200, + "shake_intensity": 50, + "music_shuffle": False, + }, + "Hard": { + "expanded_pool": True, + "trap_chance": 100, + "death_link": True, + "instant_saving": True, + "sprint_availability": SprintAvailability.option_never_available, + "cliff_weapon_upgrade": CliffWeaponUpgrade.option_always_upgraded, + "ace_weapon_upgrade": AceWeaponUpgrade.option_never_upgraded, + "iframes_duration": 50, + "shake_intensity": 100, + "music_shuffle": False, + } +} diff --git a/worlds/saving_princess/Regions.py b/worlds/saving_princess/Regions.py new file mode 100644 index 000000000000..b67bda9b2784 --- /dev/null +++ b/worlds/saving_princess/Regions.py @@ -0,0 +1,110 @@ +from typing import List, Dict + +from BaseClasses import MultiWorld, Region, Entrance + +from . import Locations +from .Constants import * + + +region_dict: Dict[str, List[str]] = { + REGION_MENU: [], + REGION_CAVE: [ + LOCATION_CAVE_AMMO, + LOCATION_CAVE_RELOAD, + LOCATION_CAVE_HEALTH, + LOCATION_CAVE_WEAPON, + EP_LOCATION_CAVE_MINIBOSS, + EP_LOCATION_CAVE_BOSS, + EVENT_LOCATION_GUARD_GONE, + ], + REGION_VOLCANIC: [ + LOCATION_VOLCANIC_RELOAD, + LOCATION_VOLCANIC_HEALTH, + LOCATION_VOLCANIC_AMMO, + LOCATION_VOLCANIC_WEAPON, + EP_LOCATION_VOLCANIC_BOSS, + EVENT_LOCATION_CLIFF_GONE, + ], + REGION_ARCTIC: [ + LOCATION_ARCTIC_AMMO, + LOCATION_ARCTIC_RELOAD, + LOCATION_ARCTIC_HEALTH, + LOCATION_ARCTIC_WEAPON, + LOCATION_JACKET, + EP_LOCATION_ARCTIC_BOSS, + EVENT_LOCATION_ACE_GONE, + ], + REGION_HUB: [ + LOCATION_HUB_AMMO, + LOCATION_HUB_HEALTH, + LOCATION_HUB_RELOAD, + EP_LOCATION_HUB_CONSOLE, + EP_LOCATION_HUB_NINJA_SCARE, + ], + REGION_SWAMP: [ + LOCATION_SWAMP_AMMO, + LOCATION_SWAMP_HEALTH, + LOCATION_SWAMP_RELOAD, + LOCATION_SWAMP_SPECIAL, + EP_LOCATION_SWAMP_BOSS, + EVENT_LOCATION_SNAKE_GONE, + ], + REGION_ELECTRICAL: [ + EP_LOCATION_ELEVATOR_NINJA_FIGHT, + LOCATION_ELECTRICAL_WEAPON, + EP_LOCATION_ELECTRICAL_MINIBOSS, + EP_LOCATION_ELECTRICAL_EXTRA, + EVENT_LOCATION_POWER_ON, + ], + REGION_ELECTRICAL_POWERED: [ + LOCATION_ELECTRICAL_RELOAD, + LOCATION_ELECTRICAL_HEALTH, + LOCATION_ELECTRICAL_AMMO, + EP_LOCATION_ELECTRICAL_BOSS, + EP_LOCATION_ELECTRICAL_FINAL_BOSS, + EVENT_LOCATION_VICTORY, + ], +} + + +def set_region_locations(region: Region, location_names: List[str], is_pool_expanded: bool): + location_pool = {**Locations.location_dict_base, **Locations.location_dict_events} + if is_pool_expanded: + location_pool = {**Locations.location_dict_expanded, **Locations.location_dict_event_expanded} + region.locations = [ + Locations.SavingPrincessLocation( + region.player, + name, + Locations.location_dict[name].code, + region + ) for name in location_names if name in location_pool.keys() + ] + + +def create_regions(multiworld: MultiWorld, player: int, is_pool_expanded: bool): + for region_name, location_names in region_dict.items(): + region = Region(region_name, player, multiworld) + set_region_locations(region, location_names, is_pool_expanded) + multiworld.regions.append(region) + connect_regions(multiworld, player) + + +def connect_regions(multiworld: MultiWorld, player: int): + # and add a connection from the menu to the hub region + menu = multiworld.get_region(REGION_MENU, player) + hub = multiworld.get_region(REGION_HUB, player) + connection = Entrance(player, f"{REGION_HUB} entrance", menu) + menu.exits.append(connection) + connection.connect(hub) + + # now add an entrance from every other region to hub + for region_name in [REGION_CAVE, REGION_VOLCANIC, REGION_ARCTIC, REGION_SWAMP, REGION_ELECTRICAL]: + connection = Entrance(player, f"{region_name} entrance", hub) + hub.exits.append(connection) + connection.connect(multiworld.get_region(region_name, player)) + + # and finally, the connection between the final region and its powered version + electrical = multiworld.get_region(REGION_ELECTRICAL, player) + connection = Entrance(player, f"{REGION_ELECTRICAL_POWERED} entrance", electrical) + electrical.exits.append(connection) + connection.connect(multiworld.get_region(REGION_ELECTRICAL_POWERED, player)) diff --git a/worlds/saving_princess/Rules.py b/worlds/saving_princess/Rules.py new file mode 100644 index 000000000000..3ee8a4f2c433 --- /dev/null +++ b/worlds/saving_princess/Rules.py @@ -0,0 +1,132 @@ +from typing import TYPE_CHECKING +from BaseClasses import CollectionState, Location, Entrance +from worlds.generic.Rules import set_rule +from .Constants import * +if TYPE_CHECKING: + from . import SavingPrincessWorld + + +def set_rules(world: "SavingPrincessWorld"): + def get_location(name: str) -> Location: + return world.get_location(name) + + def get_region_entrance(name: str) -> Entrance: + return world.get_entrance(f"{name} entrance") + + def can_hover(state: CollectionState) -> bool: + # portia can hover if she has a weapon other than the powered blaster and 4 reload speed upgrades + return ( + state.has(ITEM_RELOAD_SPEED, world.player, 4) + and state.has_any({ITEM_WEAPON_FIRE, ITEM_WEAPON_ICE, ITEM_WEAPON_VOLT}, world.player) + ) + + # guarantees that the player will have some upgrades before having to face the area bosses, except for cave + def nice_check(state: CollectionState) -> bool: + return ( + state.has(ITEM_MAX_HEALTH, world.player) + and state.has(ITEM_MAX_AMMO, world.player) + and state.has(ITEM_RELOAD_SPEED, world.player, 2) + ) + + # same as above, but for the final area + def super_nice_check(state: CollectionState) -> bool: + return ( + state.has(ITEM_MAX_HEALTH, world.player, 2) + and state.has(ITEM_MAX_AMMO, world.player, 2) + and state.has(ITEM_RELOAD_SPEED, world.player, 4) + and state.has(ITEM_WEAPON_CHARGE, world.player) + # at least one special weapon, other than powered blaster + and state.has_any({ITEM_WEAPON_FIRE, ITEM_WEAPON_ICE, ITEM_WEAPON_VOLT}, world.player) + ) + + # all special weapons required so that the boss' weapons can be targeted + def all_weapons(state: CollectionState) -> bool: + return state.has_all({ITEM_WEAPON_FIRE, ITEM_WEAPON_ICE, ITEM_WEAPON_VOLT}, world.player) + + def is_gate_unlocked(state: CollectionState) -> bool: + # the gate unlocks with all 4 boss keys, although this only applies to extended pool + if world.is_pool_expanded: + # in expanded, the final area requires all the boss keys + return ( + state.has_all( + {EP_ITEM_GUARD_GONE, EP_ITEM_CLIFF_GONE, EP_ITEM_ACE_GONE, EP_ITEM_SNAKE_GONE}, + world.player + ) and super_nice_check(state) + ) + else: + # in base pool, check that the main area bosses can be defeated + return state.has_all( + {EVENT_ITEM_GUARD_GONE, EVENT_ITEM_CLIFF_GONE, EVENT_ITEM_ACE_GONE, EVENT_ITEM_SNAKE_GONE}, + world.player + ) and super_nice_check(state) + + def is_power_on(state: CollectionState) -> bool: + # in expanded pool, the power item is what determines this, else it happens when the generator is powered + return state.has(EP_ITEM_POWER_ON if world.is_pool_expanded else EVENT_ITEM_POWER_ON, world.player) + + # set the location rules + # this is behind the blast door to arctic + set_rule(get_location(LOCATION_HUB_AMMO), lambda state: state.has(ITEM_WEAPON_CHARGE, world.player)) + # these are behind frozen doors + for location_name in [LOCATION_ARCTIC_HEALTH, LOCATION_JACKET]: + set_rule(get_location(location_name), lambda state: state.has(ITEM_WEAPON_FIRE, world.player)) + # these would require damage boosting otherwise + set_rule(get_location(LOCATION_VOLCANIC_RELOAD), + lambda state: state.has(ITEM_WEAPON_ICE, world.player) or can_hover(state)) + set_rule(get_location(LOCATION_SWAMP_AMMO), lambda state: can_hover(state)) + if world.is_pool_expanded: + # does not spawn until the guard has been defeated + set_rule(get_location(EP_LOCATION_HUB_NINJA_SCARE), lambda state: state.has(EP_ITEM_GUARD_GONE, world.player)) + # generator cannot be turned on without the volt laser + set_rule( + get_location(EP_LOCATION_ELECTRICAL_EXTRA if world.is_pool_expanded else EVENT_LOCATION_POWER_ON), + lambda state: state.has(ITEM_WEAPON_VOLT, world.player) + ) + # the roller is not very intuitive to get past without 4 ammo + set_rule(get_location(LOCATION_CAVE_WEAPON), lambda state: state.has(ITEM_MAX_AMMO, world.player)) + set_rule( + get_location(EP_LOCATION_CAVE_BOSS if world.is_pool_expanded else EVENT_LOCATION_GUARD_GONE), + lambda state: state.has(ITEM_MAX_AMMO, world.player) + ) + + # guarantee some upgrades to be found before bosses + boss_locations = [LOCATION_VOLCANIC_WEAPON, LOCATION_ARCTIC_WEAPON, LOCATION_SWAMP_SPECIAL] + if world.is_pool_expanded: + boss_locations += [EP_LOCATION_VOLCANIC_BOSS, EP_LOCATION_ARCTIC_BOSS, EP_LOCATION_SWAMP_BOSS] + else: + boss_locations += [EVENT_LOCATION_CLIFF_GONE, EVENT_LOCATION_ACE_GONE, EVENT_LOCATION_SNAKE_GONE] + for location_name in boss_locations: + set_rule(get_location(location_name), lambda state: nice_check(state)) + + # set the basic access rules for the regions, these are all behind blast doors + for region_name in [REGION_VOLCANIC, REGION_ARCTIC, REGION_SWAMP]: + set_rule(get_region_entrance(region_name), lambda state: state.has(ITEM_WEAPON_CHARGE, world.player)) + + # now for the final area regions, which have different rules based on if ep is on + set_rule(get_region_entrance(REGION_ELECTRICAL), lambda state: is_gate_unlocked(state)) + set_rule(get_region_entrance(REGION_ELECTRICAL_POWERED), lambda state: is_power_on(state)) + + # brainos requires all weapons, cannot destroy the cannons otherwise + if world.is_pool_expanded: + set_rule(get_location(EP_LOCATION_ELECTRICAL_FINAL_BOSS), lambda state: all_weapons(state)) + # and we need to beat brainos to beat the game + set_rule(get_location(EVENT_LOCATION_VICTORY), lambda state: all_weapons(state)) + + # if not expanded pool, place the events for the boss kills and generator + if not world.is_pool_expanded: + # accessible with no items + cave_item = world.create_item(EVENT_ITEM_GUARD_GONE) + get_location(EVENT_LOCATION_GUARD_GONE).place_locked_item(cave_item) + volcanic_item = world.create_item(EVENT_ITEM_CLIFF_GONE) + get_location(EVENT_LOCATION_CLIFF_GONE).place_locked_item(volcanic_item) + arctic_item = world.create_item(EVENT_ITEM_ACE_GONE) + get_location(EVENT_LOCATION_ACE_GONE).place_locked_item(arctic_item) + swamp_item = world.create_item(EVENT_ITEM_SNAKE_GONE) + get_location(EVENT_LOCATION_SNAKE_GONE).place_locked_item(swamp_item) + power_item = world.create_item(EVENT_ITEM_POWER_ON) + get_location(EVENT_LOCATION_POWER_ON).place_locked_item(power_item) + + # and, finally, set the victory event + victory_item = world.create_item(EVENT_ITEM_VICTORY) + get_location(EVENT_LOCATION_VICTORY).place_locked_item(victory_item) + world.multiworld.completion_condition[world.player] = lambda state: state.has(EVENT_ITEM_VICTORY, world.player) diff --git a/worlds/saving_princess/__init__.py b/worlds/saving_princess/__init__.py new file mode 100644 index 000000000000..4109f356fd2e --- /dev/null +++ b/worlds/saving_princess/__init__.py @@ -0,0 +1,174 @@ +from typing import ClassVar, Dict, Any, Type, List, Union + +import Utils +from BaseClasses import Tutorial, ItemClassification as ItemClass +from Options import PerGameCommonOptions, OptionError +from settings import Group, UserFilePath, LocalFolderPath, Bool +from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import components, Component, launch_subprocess, Type as ComponentType +from . import Options, Items, Locations +from .Constants import * + + +def launch_client(*args: str): + from .Client import launch + launch_subprocess(launch(*args), name=CLIENT_NAME) + + +components.append( + Component(f"{GAME_NAME} Client", game_name=GAME_NAME, func=launch_client, component_type=ComponentType.CLIENT, supports_uri=True) +) + + +class SavingPrincessSettings(Group): + class GamePath(UserFilePath): + """Path to the game executable from which files are extracted""" + description = "the Saving Princess game executable" + is_exe = True + md5s = [GAME_HASH] + + class InstallFolder(LocalFolderPath): + """Path to the mod installation folder""" + description = "the folder to install Saving Princess Archipelago to" + + class LaunchGame(Bool): + """Set this to false to never autostart the game""" + + class LaunchCommand(str): + """ + The console command that will be used to launch the game + The command will be executed with the installation folder as the current directory + """ + + exe_path: GamePath = GamePath("Saving Princess.exe") + install_folder: InstallFolder = InstallFolder("Saving Princess") + launch_game: Union[LaunchGame, bool] = True + launch_command: LaunchCommand = LaunchCommand('"Saving Princess v0_8.exe"' if Utils.is_windows + else 'wine "Saving Princess v0_8.exe"') + + +class SavingPrincessWeb(WebWorld): + theme = "partyTime" + bug_report_page = "https://github.com/LeonarthCG/saving-princess-archipelago/issues" + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Saving Princess for Archipelago multiworld.", + "English", + "setup_en.md", + "setup/en", + ["LeonarthCG"] + ) + tutorials = [setup_en] + options_presets = Options.presets + option_groups = Options.groups + + +class SavingPrincessWorld(World): + """ + Explore a space station crawling with rogue machines and even rival bounty hunters + with the same objective as you - but with far, far different intentions! + + Expand your arsenal as you collect upgrades to your trusty arm cannon and armor! + """ # Excerpt from itch + game = GAME_NAME + web = SavingPrincessWeb() + required_client_version = (0, 5, 0) + + topology_present = False + + item_name_to_id = { + key: value.code for key, value in (Items.item_dict.items() - Items.item_dict_events.items()) + } + location_name_to_id = { + key: value.code for key, value in (Locations.location_dict.items() - Locations.location_dict_events.items()) + } + + item_name_groups = { + "Weapons": {key for key in Items.item_dict_weapons.keys()}, + "Upgrades": {key for key in Items.item_dict_upgrades.keys()}, + "Keys": {key for key in Items.item_dict_keys.keys()}, + "Filler": {key for key in Items.item_dict_filler.keys()}, + "Traps": {key for key in Items.item_dict_traps.keys()}, + } + + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = Options.SavingPrincessOptions + options: Options.SavingPrincessOptions + settings_key = "saving_princess_settings" + settings: ClassVar[SavingPrincessSettings] + + is_pool_expanded: bool = False + music_table: List[int] = list(range(16)) + + def generate_early(self) -> None: + if not self.player_name.isascii(): + raise OptionError(f"{self.player_name}'s name must be only ASCII.") + self.is_pool_expanded = self.options.expanded_pool > 0 + if self.options.music_shuffle: + self.random.shuffle(self.music_table) + # find zzz and purple and swap them back to their original positions + for song_id in [9, 13]: + song_index = self.music_table.index(song_id) + t = self.music_table[song_id] + self.music_table[song_id] = song_id + self.music_table[song_index] = t + + def create_regions(self) -> None: + from .Regions import create_regions + create_regions(self.multiworld, self.player, self.is_pool_expanded) + + def create_items(self) -> None: + items_made: int = 0 + + # now, for each item + item_dict = Items.item_dict_expanded if self.is_pool_expanded else Items.item_dict_base + for item_name, item_data in item_dict.items(): + # create count copies of the item + for i in range(item_data.count): + self.multiworld.itempool.append(self.create_item(item_name)) + items_made += item_data.count + # and create count_extra useful copies of the item + original_item_class: ItemClass = item_data.item_class + item_data.item_class = ItemClass.useful + for i in range(item_data.count_extra): + self.multiworld.itempool.append(self.create_item(item_name)) + item_data.item_class = original_item_class + items_made += item_data.count_extra + + # get the number of unfilled locations, that is, locations for items - items generated + location_count = len(Locations.location_dict_base) + if self.is_pool_expanded: + location_count = len(Locations.location_dict_expanded) + junk_count: int = location_count - items_made + + # and generate as many junk items as unfilled locations + for i in range(junk_count): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) + + def create_item(self, name: str) -> Items.SavingPrincessItem: + return Items.item_dict[name].create_item(self.player) + + def get_filler_item_name(self) -> str: + filler_list = list(Items.item_dict_filler.keys()) + # check if this is going to be a trap + if self.random.randint(0, 99) < self.options.trap_chance: + filler_list = list(Items.item_dict_traps.keys()) + # and return one of the names at random + return self.random.choice(filler_list) + + def set_rules(self): + from .Rules import set_rules + set_rules(self) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict( + "death_link", + "expanded_pool", + "instant_saving", + "sprint_availability", + "cliff_weapon_upgrade", + "ace_weapon_upgrade", + "shake_intensity", + "iframes_duration", + ) + slot_data["music_table"] = self.music_table + return slot_data diff --git a/worlds/saving_princess/docs/en_Saving Princess.md b/worlds/saving_princess/docs/en_Saving Princess.md new file mode 100644 index 000000000000..3eb6b9831c38 --- /dev/null +++ b/worlds/saving_princess/docs/en_Saving Princess.md @@ -0,0 +1,55 @@ +# Saving Princess + +## Quick Links +- [Setup Guide](/tutorial/Saving%20Princess/setup/en) +- [Options Page](/games/Saving%20Princess/player-options) +- [Saving Princess Archipelago GitHub](https://github.com/LeonarthCG/saving-princess-archipelago) + +## What changes have been made? + +The game has had several changes made to add new features and prevent issues. The most important changes are the following: +- There is an in-game connection settings menu, autotracker and client console. +- New save files are created and used automatically for each seed and slot played. +- The game window can now be dragged and a new integer scaling option has been added. + +## What items and locations get shuffled? + +The chest contents and special weapons are the items and locations that get shuffled. + +Additionally, there are new items to work as filler and traps, ranging from a full health and ammo restore to spawning a Ninja on top of you. + +The Expanded Pool option, which is enabled by default, adds a few more items and locations: +- Completing the intro sequence, powering the generator with the Volt Laser and defeating each boss become locations. +- 4 Keys will be shuffled, which serve to open the door to the final area in place of defeating the main area bosses. +- A System Power item will be shuffled, which restores power to the final area instead of this happening when the generator is powered. + +## What does another world's item look like in Saving Princess? + +Some locations, such as boss kills, have no visual representation, but those that do will have the Archipelago icon. + +Once the item is picked up, a textbox will inform you of the item that was found as well as the player that will be receiving it. + +These textboxes will have colored backgrounds and comments about the item category. +For example, progression items will have a purple background and say "Looks plenty important!". + +## When the player receives an item, what happens? + +When you receive an item, a textbox will show up. +This textbox shows both which item you got and which player sent it to you. + +If you send an item to yourself, however, the sending player will be omitted. + +## Unique Local Commands + +The following commands are only available when using the in-game console in Saving Princess: +- `/help` Returns the help listing. +- `/options` Lists currently applied options. +- `/resync` Manually triggers a resync. This also resends all found locations. +- `/unstuck` Sets save point to the first save point. Portia is then killed. +- `/deathlink [on|off]` Toggles or sets death link mode. +- `/instantsaving [on|off]` Toggles or sets instant saving. +- `/sprint {never|always|jacket}` Sets sprint mode. +- `/cliff {never|always|vanilla}` Sets Cliff's weapon upgrade condition. +- `/ace {never|always|vanilla}` Sets Ace's weapon upgrade condition. +- `/iframes n` Sets the iframe duration % multiplier to n, where 0 <= n <= 400. +- `/shake n` Sets the shake intensity % multiplier to n, where 0 <= n <= 100. diff --git a/worlds/saving_princess/docs/setup_en.md b/worlds/saving_princess/docs/setup_en.md new file mode 100644 index 000000000000..5f7cfb49f560 --- /dev/null +++ b/worlds/saving_princess/docs/setup_en.md @@ -0,0 +1,148 @@ +# Saving Princess Setup Guide + +## Quick Links +- [Game Info](/games/Saving%20Princess/info/en) +- [Options Page](/games/Saving%20Princess/player-options) +- [Saving Princess Archipelago GitHub](https://github.com/LeonarthCG/saving-princess-archipelago) + +## Installation Procedures + +### Automated Installation + +*These instructions have only been tested on Windows and Ubuntu.* + +Once everything is set up, it is recommended to continue launching the game through this method, as it will check for any updates to the mod and automatically apply them. +This is also the method used by the Automatic Connection described further below. + +1. Purchase and download [Saving Princess](https://brainos.itch.io/savingprincess) +2. Download and install the latest [Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases/latest) +3. Launch `ArchipelagoLauncher` and click on "Saving Princess Client" + * You will probably need to scroll down on the Clients column to see it +4. Follow the prompts + * On Linux, you will need one of either Wine or 7z for the automated installation + +When launching the game, Windows machines will simply run the executable. For any other OS, the launcher defaults to trying to run the game through Wine. You can change this by modifying the `launch_command` in `options.yaml` or `host.yaml`, under the `saving_princess_settings` section. + +### Manual Windows Installation + +Required software: +- Saving Princess, found at its [itch.io Store Page](https://brainos.itch.io/savingprincess) +- `saving_princess_basepatch.bsdiff4` and `gm-apclientpp.dll`, from [saving_princess_archipelago.zip](https://github.com/LeonarthCG/saving-princess-archipelago/releases/latest) +- Software that can decompress the previous files, such as [7-zip](https://www.7-zip.org/download.html) +- A way to apply `.bsdiff4` patches, such as [bspatch](https://www.romhacking.net/utilities/929/) + +Steps: +1. Extract all files from `Saving Princess.exe`, as if it were a `.7z` file + * Feel free to rename `Saving Princess.exe` to `Saving Princess.exe.7z` if needed + * If installed through the itch app, you can find the installation directory from the game's page, pressing the cog button, then "Manage" and finally "Open folder in explorer" +2. Extract all files from `saving_princess_archipelago.zip` into the same directory as the files extracted in the previous step + * This should include, at least, `saving_princess_basepatch.bsdiff4` and `gm-apclientpp.dll` +3. If you don't have `original_data.win`, copy `data.win` and rename its copy to `original_data.win` + * By keeping an unmodified copy of `data.win`, you will have an easier time updating in the future +4. Apply the `saving_princess_basepatch.bsdiff4` patch using your patching software +5. To launch the game, run `Saving Princess v0_8.exe` + +### Manual Linux Installation + +*These instructions have only been tested on Ubuntu.* + +The game does run mostly well through Wine, so it is possible to play on Linux, although there are some minor sprite displacement and sound issues from time to time. + +You can follow the instructions for Windows with very few changes: + +* Using the `p7zip-full` package to decompress the file. +``` +7z e 'Saving Princess.exe' +``` +* And the `bsdiff` package for patching. +``` +bspatch original_data.win data.win saving_princess_basepatch.bsdiff4 +``` + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). + +### Where do I get a YAML file? + +You can customize your options by visiting the [Saving Princess Player Options Page](/games/Saving%20Princess/player-options). + +### Verifying your YAML file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/check). + +## Joining a MultiWorld Game + +### Automatic Connection on archipelago.gg + +1. Go to the room page of the MultiWorld you are going to join. +2. Click on your slot name on the left side. +3. Click the "Saving Princess Client" button in the prompt. + * This launches the same client described in the Automated Installation section. +4. Upon reaching the title screen, a connection attempt will automatically be started. + +Note that this updates your Saving Princess saved connection details, which are described in the Manual Connection section. + +### Manual Connection + +After launching the game, enter the Archipelago options menu through the in-game button with the Archipelago icon. +From here, enter the different menus and type in the following details in their respective fields: +- **server:port** (e.g. `archipelago.gg:38281`) + * If hosting on the website, this detail will be shown in your created room. +- **slot name** (e.g. `Player`) + * This is your player name, which you chose along with your player options. +- **password** (e.g. `123456`) + * If the room does not have a password, it can be left empty. + +This configuration persists through launches and even updates. + +With your settings filled, start a connection attempt by pressing on the title screen's "CONNECT!" button. + +Once connected, the button will become one of either "NEW GAME" or "CONTINUE". +The game automatically keeps a save file for each seed and slot combination, so you do not need to manually move or delete save files. + +All that's left is pressing on the button again to start playing. If you are waiting for a countdown, press "NEW GAME" when the countdown finishes. + +## Gameplay Questions + +### Do I need to save the game before I stop playing? + +It is safe to close the game at any point while playing, your progress will be kept. + +### What happens if I lose connection? + +If a disconnection occurs, you will see the HUD connection indicator go grey. +From here, the game will automatically try to reconnect. +You can tell it succeeded if the indicator regains its color. + +If the game is unable to reconnect, save and restart. + +Although you can keep playing while disconnected, you won't get any items until you reconnect, not even items found in your own game. +Once reconnected, however, all of your progress will sync up. + +### I got an item, but it did not say who sent it to me + +Items sent to you by yourself do not list the sender. + +Additionally, if you get an item while already having the max for that item (for example, you have 9 ammo and get sent a Clip Extension), no message will be shown at all. + +### I pressed the release/collect button, but nothing happened + +It is likely that you do not have release or collect permissions, or that there is nothing to release or collect. +Another option is that your connection was interrupted. + +If you would still like to use release or collect, refer to [this section of the server commands page](https://archipelago.gg/tutorial/Archipelago/commands/en#collect/release). + +You may use the in-game console to execute the commands, if your slot has permissions to do so. + +### I am trying to configure my controller, but the menu keeps closing itself + +Steam Input will make your controller behave as a keyboard and mouse even while not playing any Steam games. + +To fix this, simply close Steam while playing Saving Princess. + +Another option is to disable Steam Input under `Steam -> Settings -> Controller -> External Gamepad Settings` From 6c69f590cf18d1224445ee86da7c85c7663f5fb7 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 8 Dec 2024 02:22:56 +0100 Subject: [PATCH 095/112] WebHost: fix host room not updating (ports in) slot table (#4308) --- WebHostLib/templates/hostRoom.html | 11 +++++++++-- WebHostLib/templates/macros.html | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 8e76dafc12fa..c5996d181ee0 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -178,8 +178,15 @@ }) .then(text => new DOMParser().parseFromString(text, 'text/html')) .then(newDocument => { - let el = newDocument.getElementById("host-room-info"); - document.getElementById("host-room-info").innerHTML = el.innerHTML; + ["host-room-info", "slots-table"].forEach(function(id) { + const newEl = newDocument.getElementById(id); + const oldEl = document.getElementById(id); + if (oldEl && newEl) { + oldEl.innerHTML = newEl.innerHTML; + } else if (newEl) { + console.warn(`Did not find element to replace for ${id}`) + } + }); }); } diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 6b2a4b0ed784..b95b8820a72f 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -8,7 +8,7 @@ {%- endmacro %} {% macro list_patches_room(room) %} {% if room.seed.slots %} - +
    From e3b5451672c694c12974801f5c89cc172db3ff5a Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Dec 2024 14:43:16 -0500 Subject: [PATCH 096/112] CI: cap pytest-subtest version (#4344) --- .github/workflows/unittests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 88b5d12987ad..9db9de9b4042 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -53,7 +53,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-subtests pytest-xdist + pip install pytest "pytest-subtests<0.14.0" pytest-xdist python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" python Launcher.py --update_settings # make sure host.yaml exists for tests - name: Unittests From a948697f3a2387d13862a194690c39af8da2dbd8 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Mon, 9 Dec 2024 00:57:34 +0000 Subject: [PATCH 097/112] Raft: Place locked items in create_items and fix get_pre_fill_items (#4250) * Raft: Place locked items in create_items and fix get_pre_fill_items `pre_fill` runs after item plando, and item plando could place an item at a location where Raft was intending to place a locked item, which would crash generation. This patch moves the placement of these locked items earlier, into `create_items`. Setting items into `multiworld.raft_frequencyItemsPerPlayer` for each player has been replaced with passing `frequencyItems` to the new `place_frequencyItems` function. `setLocationItem` and `setLocationItemFromRegion` have been moved into the new `place_frequencyItems` function so that they can capture the `frequencyItems` argument variable. The `get_pre_fill_items` function could return a list of all previously placed items across the entire multiworld which was not correct. It should have returned the items in `multiworld.raft_frequencyItemsPerPlayer[self.player]`. Now that these items are placed in `create_items` instead of `pre_fill`, `get_pre_fill_items` is no longer necessary and has been removed. * self.multiworld.get_location -> self.get_location Changed the occurences in the modified code. --- worlds/raft/__init__.py | 68 +++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 71d5d1c7e44b..3e33b417c04b 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -57,10 +57,6 @@ def create_items(self): frequencyItems.append(raft_item) else: pool.append(raft_item) - if isFillingFrequencies: - if not hasattr(self.multiworld, "raft_frequencyItemsPerPlayer"): - self.multiworld.raft_frequencyItemsPerPlayer = {} - self.multiworld.raft_frequencyItemsPerPlayer[self.player] = frequencyItems extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot @@ -109,17 +105,15 @@ def create_items(self): self.multiworld.get_location("Utopia Complete", self.player).place_locked_item( RaftItem("Victory", ItemClassification.progression, None, player=self.player)) + if frequencyItems: + self.place_frequencyItems(frequencyItems) + def set_rules(self): set_rules(self.multiworld, self.player) def create_regions(self): create_regions(self.multiworld, self.player) - def get_pre_fill_items(self): - if self.options.island_frequency_locations.is_filling_frequencies_in_world(): - return [loc.item for loc in self.multiworld.get_filled_locations()] - return [] - def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) @@ -152,23 +146,34 @@ def collect_item(self, state, item, remove=False): return super(RaftWorld, self).collect_item(state, item, remove) - def pre_fill(self): + def place_frequencyItems(self, frequencyItems): + def setLocationItem(location: str, itemName: str): + itemToUse = next(filter(lambda itm: itm.name == itemName, frequencyItems)) + frequencyItems.remove(itemToUse) + self.get_location(location).place_locked_item(itemToUse) + + def setLocationItemFromRegion(region: str, itemName: str): + itemToUse = next(filter(lambda itm: itm.name == itemName, frequencyItems)) + frequencyItems.remove(itemToUse) + location = self.random.choice(list(loc for loc in location_table if loc["region"] == region)) + self.get_location(location["name"]).place_locked_item(itemToUse) + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_vanilla: - self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") - self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") - self.setLocationItem("Relay Station quest", "Caravan Island Frequency") - self.setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency") - self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") - self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") - self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") + setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") + setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") + setLocationItem("Relay Station quest", "Caravan Island Frequency") + setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency") + setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") + setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") + setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island: - self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") - self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") - self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") - self.setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency") - self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") - self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") - self.setLocationItemFromRegion("Temperance", "Utopia Frequency") + setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") + setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") + setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") + setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency") + setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") + setLocationItemFromRegion("Varuna Point", "Temperance Frequency") + setLocationItemFromRegion("Temperance", "Utopia Frequency") elif self.options.island_frequency_locations in [ self.options.island_frequency_locations.option_random_island_order, self.options.island_frequency_locations.option_random_on_island_random_order @@ -201,22 +206,11 @@ def pre_fill(self): currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) if self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_island_order: - self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) + setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island_random_order: - self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) + setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) previousLocation = currentLocation - def setLocationItem(self, location: str, itemName: str): - itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player])) - self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse) - self.multiworld.get_location(location, self.player).place_locked_item(itemToUse) - - def setLocationItemFromRegion(self, region: str, itemName: str): - itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player])) - self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse) - location = self.random.choice(list(loc for loc in location_table if loc["region"] == region)) - self.multiworld.get_location(location["name"], self.player).place_locked_item(itemToUse) - def fill_slot_data(self): return { "IslandGenerationDistance": self.options.island_generation_distance.value, From 5b4d7c752670b9cdf79258792e8045d968a54e96 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 8 Dec 2024 19:58:49 -0500 Subject: [PATCH 098/112] TUNIC: Add Shield to Ladder Storage logic (#4146) --- worlds/tunic/__init__.py | 5 +++++ worlds/tunic/options.py | 2 +- worlds/tunic/rules.py | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index d1430aac1895..4c62b18b140f 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -286,6 +286,11 @@ def remove_filler(amount: int) -> None: tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful)) items_to_create[page] = 0 + # logically relevant if you have ladder storage enabled + if self.options.ladder_storage and not self.options.ladder_storage_without_items: + tunic_items.append(self.create_item("Shield", ItemClassification.progression)) + items_to_create["Shield"] = 0 + if self.options.maskless: tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) items_to_create["Scavenger Mask"] = 0 diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index cdd37a889461..f1d53362f4c9 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -216,7 +216,7 @@ class LadderStorage(Choice): class LadderStorageWithoutItems(Toggle): """ - If disabled, you logically require Stick, Sword, or Magic Orb to perform Ladder Storage. + If disabled, you logically require Stick, Sword, Magic Orb, or Shield to perform Ladder Storage. If enabled, you will be expected to perform Ladder Storage without progression items. This can be done with the plushie code, a Golden Coin, Prayer, and many other options. diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index aa69666daeb6..58c987acbcee 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -18,6 +18,7 @@ prayer = "Pages 24-25 (Prayer)" holy_cross = "Pages 42-43 (Holy Cross)" icebolt = "Pages 52-53 (Icebolt)" +shield = "Shield" key = "Key" house_key = "Old House Key" vault_key = "Fortress Vault Key" @@ -82,7 +83,7 @@ def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: return False if world.options.ladder_storage_without_items: return True - return has_stick(state, world.player) or state.has(grapple, world.player) + return has_stick(state, world.player) or state.has_any((grapple, shield), world.player) def has_mask(state: CollectionState, world: "TunicWorld") -> bool: From 1f712d9a8754103e2bbeb13075f460ff366d55df Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Dec 2024 19:59:40 -0500 Subject: [PATCH 099/112] Various Worlds: use / explicitly for pkgutil (#4232) --- worlds/kdl3/regions.py | 2 +- worlds/kdl3/rom.py | 8 ++++---- worlds/ladx/LADXR/patches/bank34.py | 2 +- worlds/ladx/LADXR/patches/bank3e.py | 2 +- worlds/lingo/static_logic.py | 2 +- worlds/minecraft/Constants.py | 2 +- worlds/mm2/rom.py | 2 +- worlds/shivers/Constants.py | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/worlds/kdl3/regions.py b/worlds/kdl3/regions.py index c47e5dee4095..af5208d365f0 100644 --- a/worlds/kdl3/regions.py +++ b/worlds/kdl3/regions.py @@ -57,7 +57,7 @@ def generate_valid_level(world: "KDL3World", level: int, stage: int, def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None: level_names = {location_name.level_names[level]: level for level in location_name.level_names} - room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) + room_data = orjson.loads(get_data(__name__, "data/Rooms.json")) rooms: Dict[str, KDL3Room] = dict() for room_entry in room_data: room = KDL3Room(room_entry["name"], world.player, world.multiworld, None, room_entry["level"], diff --git a/worlds/kdl3/rom.py b/worlds/kdl3/rom.py index 3dd10ce1c43f..741ea0083027 100644 --- a/worlds/kdl3/rom.py +++ b/worlds/kdl3/rom.py @@ -313,7 +313,7 @@ def handle_level_sprites(stages: List[Tuple[int, ...]], sprites: List[bytearray] def write_heart_star_sprites(rom: RomData) -> None: compressed = rom.read_bytes(heart_star_address, heart_star_size) decompressed = hal_decompress(compressed) - patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) + patch = get_data(__name__, "data/APHeartStar.bsdiff4") patched = bytearray(bsdiff4.patch(decompressed, patch)) rom.write_bytes(0x1AF7DF, patched) patched[0:0] = [0xE3, 0xFF] @@ -327,10 +327,10 @@ def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool) -> No decompressed = hal_decompress(compressed) patched = bytearray(decompressed) if consumables: - patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) + patch = get_data(__name__, "data/APConsumable.bsdiff4") patched = bytearray(bsdiff4.patch(bytes(patched), patch)) if stars: - patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) + patch = get_data(__name__, "data/APStars.bsdiff4") patched = bytearray(bsdiff4.patch(bytes(patched), patch)) patched[0:0] = [0xE3, 0xFF] patched.append(0xFF) @@ -380,7 +380,7 @@ def get_source_data(cls) -> bytes: def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None: patch.write_file("kdl3_basepatch.bsdiff4", - get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) + get_data(__name__, "data/kdl3_basepatch.bsdiff4")) # Write open world patch if world.options.open_world: diff --git a/worlds/ladx/LADXR/patches/bank34.py b/worlds/ladx/LADXR/patches/bank34.py index 31b9ca124436..e88727e868c6 100644 --- a/worlds/ladx/LADXR/patches/bank34.py +++ b/worlds/ladx/LADXR/patches/bank34.py @@ -75,7 +75,7 @@ def addBank34(rom, item_list): .notCavesA: add hl, de ret - """ + pkgutil.get_data(__name__, os.path.join("bank3e.asm", "message.asm")).decode().replace("\r", ""), 0x4000), fill_nop=True) + """ + pkgutil.get_data(__name__, "bank3e.asm/message.asm").decode().replace("\r", ""), 0x4000), fill_nop=True) nextItemLookup = ItemNameStringBufferStart nameLookup = { diff --git a/worlds/ladx/LADXR/patches/bank3e.py b/worlds/ladx/LADXR/patches/bank3e.py index 7e690349a335..632fffa7e63e 100644 --- a/worlds/ladx/LADXR/patches/bank3e.py +++ b/worlds/ladx/LADXR/patches/bank3e.py @@ -56,7 +56,7 @@ def addBank3E(rom, seed, player_id, player_name_list): """)) def get_asm(name): - return pkgutil.get_data(__name__, os.path.join("bank3e.asm", name)).decode().replace("\r", "") + return pkgutil.get_data(__name__, "bank3e.asm/" + name).decode().replace("\r", "") rom.patch(0x3E, 0x0000, 0x2F00, ASM(""" call MainJumpTable diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index 74eea449f228..9925e9582a2c 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -107,7 +107,7 @@ def find_class(self, module, name): return getattr(safe_builtins, name) raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") - file = pkgutil.get_data(__name__, os.path.join("data", "generated.dat")) + file = pkgutil.get_data(__name__, "data/generated.dat") pickdata = RenameUnpickler(BytesIO(file)).load() HASHES.update(pickdata["HASHES"]) diff --git a/worlds/minecraft/Constants.py b/worlds/minecraft/Constants.py index 0d1101e802fd..1f7b6fa6acef 100644 --- a/worlds/minecraft/Constants.py +++ b/worlds/minecraft/Constants.py @@ -3,7 +3,7 @@ import pkgutil def load_data_file(*args) -> dict: - fname = os.path.join("data", *args) + fname = "/".join(["data", *args]) return json.loads(pkgutil.get_data(__name__, fname).decode()) # For historical reasons, these values are different. diff --git a/worlds/mm2/rom.py b/worlds/mm2/rom.py index cac0a8706007..e37c5bc2a148 100644 --- a/worlds/mm2/rom.py +++ b/worlds/mm2/rom.py @@ -126,7 +126,7 @@ def write_bytes(self, offset: int, value: Iterable[int]) -> None: def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None: - patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm2_basepatch.bsdiff4"))) + patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mm2_basepatch.bsdiff4")) # text writing patch.write_bytes(0x37E2A, MM2TextEntry("FOR ", 0xCB).resolve()) patch.write_bytes(0x37EAA, MM2TextEntry("GET EQUIPPED ", 0x0B).resolve()) diff --git a/worlds/shivers/Constants.py b/worlds/shivers/Constants.py index 0b00cecec3ec..95b3c2d56ad9 100644 --- a/worlds/shivers/Constants.py +++ b/worlds/shivers/Constants.py @@ -3,7 +3,7 @@ import pkgutil def load_data_file(*args) -> dict: - fname = os.path.join("data", *args) + fname = "/".join(["data", *args]) return json.loads(pkgutil.get_data(__name__, fname).decode()) location_id_offset: int = 27000 From 26f9720e69d33af5f55950d4bc197460f39df3ab Mon Sep 17 00:00:00 2001 From: Louis M Date: Sun, 8 Dec 2024 20:18:00 -0500 Subject: [PATCH 100/112] Aquaria: mega refactoring (#3810) This PR is mainly refactoring. Here is what changed: - Changing item names so that each words are capitalized (`Energy Form` instead of `Energy form`) - Removing duplication of string literal by using: - Constants for items and locations, - Region's name attribute for entrances, - Clarify some documentations, - Adding some region to be more representative of the game and to remove listing of locations in the rules (prioritize entrance rules over individual location rules). This is the other minor modifications that are not refactoring: - Adding an early bind song option since that can be used to exit starting area. - Changing Sun God to Lumerean God to be coherent with the other gods. - Changing Home Water to Home Waters and Open Water to Open Waters to be coherent with the game. - Removing a rules to have an attack to go in Mithalas Cathedral since you can to get some checks in it without an attack. - Adding some options to slot data to be used with Poptracker. - Fixing a little but still potentially logic breaking bug. --- worlds/aquaria/Items.py | 440 ++++-- worlds/aquaria/Locations.py | 800 +++++++---- worlds/aquaria/Options.py | 52 +- worlds/aquaria/Regions.py | 1190 ++++++++--------- worlds/aquaria/__init__.py | 82 +- worlds/aquaria/docs/en_Aquaria.md | 2 +- worlds/aquaria/test/__init__.py | 405 +++--- worlds/aquaria/test/test_beast_form_access.py | 24 +- ...test_beast_form_or_arnassi_armor_access.py | 46 +- worlds/aquaria/test/test_bind_song_access.py | 33 +- .../test/test_bind_song_option_access.py | 40 +- .../aquaria/test/test_confined_home_water.py | 9 +- worlds/aquaria/test/test_dual_song_access.py | 17 +- .../aquaria/test/test_energy_form_access.py | 29 +- .../test_energy_form_or_dual_form_access.py | 132 +- worlds/aquaria/test/test_fish_form_access.py | 39 +- worlds/aquaria/test/test_li_song_access.py | 55 +- worlds/aquaria/test/test_light_access.py | 99 +- .../aquaria/test/test_nature_form_access.py | 79 +- ...st_no_progression_hard_hidden_locations.py | 51 +- .../test_progression_hard_hidden_locations.py | 51 +- .../aquaria/test/test_spirit_form_access.py | 38 +- worlds/aquaria/test/test_sun_form_access.py | 24 +- .../test_unconfine_home_water_via_both.py | 9 +- ...st_unconfine_home_water_via_energy_door.py | 9 +- ...st_unconfine_home_water_via_transturtle.py | 9 +- 26 files changed, 2119 insertions(+), 1645 deletions(-) diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py index f822d675e6e7..88ac7c76e0a3 100644 --- a/worlds/aquaria/Items.py +++ b/worlds/aquaria/Items.py @@ -59,156 +59,316 @@ class ItemData: type: ItemType group: ItemGroup - def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup): + def __init__(self, aId: int, count: int, aType: ItemType, group: ItemGroup): """ Initialisation of the item data - @param id: The item ID + @param aId: The item ID @param count: the number of items in the pool - @param type: the importance type of the item + @param aType: the importance type of the item @param group: the usage of the item in the game """ - self.id = id + self.id = aId self.count = count - self.type = type + self.type = aType self.group = group +class ItemNames: + """ + Constants used to represent the mane of every items. + """ + # Normal items + ANEMONE = "Anemone" + ARNASSI_STATUE = "Arnassi Statue" + BIG_SEED = "Big Seed" + GLOWING_SEED = "Glowing Seed" + BLACK_PEARL = "Black Pearl" + BABY_BLASTER = "Baby Blaster" + CRAB_ARMOR = "Crab Armor" + BABY_DUMBO = "Baby Dumbo" + TOOTH = "Tooth" + ENERGY_STATUE = "Energy Statue" + KROTITE_ARMOR = "Krotite Armor" + GOLDEN_STARFISH = "Golden Starfish" + GOLDEN_GEAR = "Golden Gear" + JELLY_BEACON = "Jelly Beacon" + JELLY_COSTUME = "Jelly Costume" + JELLY_PLANT = "Jelly Plant" + MITHALAS_DOLL = "Mithalas Doll" + MITHALAN_DRESS = "Mithalan Dress" + MITHALAS_BANNER = "Mithalas Banner" + MITHALAS_POT = "Mithalas Pot" + MUTANT_COSTUME = "Mutant Costume" + BABY_NAUTILUS = "Baby Nautilus" + BABY_PIRANHA = "Baby Piranha" + ARNASSI_ARMOR = "Arnassi Armor" + SEED_BAG = "Seed Bag" + KING_S_SKULL = "King's Skull" + SONG_PLANT_SPORE = "Song Plant Spore" + STONE_HEAD = "Stone Head" + SUN_KEY = "Sun Key" + GIRL_COSTUME = "Girl Costume" + ODD_CONTAINER = "Odd Container" + TRIDENT = "Trident" + TURTLE_EGG = "Turtle Egg" + JELLY_EGG = "Jelly Egg" + URCHIN_COSTUME = "Urchin Costume" + BABY_WALKER = "Baby Walker" + VEDHA_S_CURE_ALL = "Vedha's Cure-All" + ZUUNA_S_PEROGI = "Zuuna's Perogi" + ARCANE_POULTICE = "Arcane Poultice" + BERRY_ICE_CREAM = "Berry Ice Cream" + BUTTERY_SEA_LOAF = "Buttery Sea Loaf" + COLD_BORSCHT = "Cold Borscht" + COLD_SOUP = "Cold Soup" + CRAB_CAKE = "Crab Cake" + DIVINE_SOUP = "Divine Soup" + DUMBO_ICE_CREAM = "Dumbo Ice Cream" + FISH_OIL = "Fish Oil" + GLOWING_EGG = "Glowing Egg" + HAND_ROLL = "Hand Roll" + HEALING_POULTICE = "Healing Poultice" + HEARTY_SOUP = "Hearty Soup" + HOT_BORSCHT = "Hot Borscht" + HOT_SOUP = "Hot Soup" + ICE_CREAM = "Ice Cream" + LEADERSHIP_ROLL = "Leadership Roll" + LEAF_POULTICE = "Leaf Poultice" + LEECHING_POULTICE = "Leeching Poultice" + LEGENDARY_CAKE = "Legendary Cake" + LOAF_OF_LIFE = "Loaf of Life" + LONG_LIFE_SOUP = "Long Life Soup" + MAGIC_SOUP = "Magic Soup" + MUSHROOM_X_2 = "Mushroom x 2" + PEROGI = "Perogi" + PLANT_LEAF = "Plant Leaf" + PLUMP_PEROGI = "Plump Perogi" + POISON_LOAF = "Poison Loaf" + POISON_SOUP = "Poison Soup" + RAINBOW_MUSHROOM = "Rainbow Mushroom" + RAINBOW_SOUP = "Rainbow Soup" + RED_BERRY = "Red Berry" + RED_BULB_X_2 = "Red Bulb x 2" + ROTTEN_CAKE = "Rotten Cake" + ROTTEN_LOAF_X_8 = "Rotten Loaf x 8" + ROTTEN_MEAT = "Rotten Meat" + ROYAL_SOUP = "Royal Soup" + SEA_CAKE = "Sea Cake" + SEA_LOAF = "Sea Loaf" + SHARK_FIN_SOUP = "Shark Fin Soup" + SIGHT_POULTICE = "Sight Poultice" + SMALL_BONE_X_2 = "Small Bone x 2" + SMALL_EGG = "Small Egg" + SMALL_TENTACLE_X_2 = "Small Tentacle x 2" + SPECIAL_BULB = "Special Bulb" + SPECIAL_CAKE = "Special Cake" + SPICY_MEAT_X_2 = "Spicy Meat x 2" + SPICY_ROLL = "Spicy Roll" + SPICY_SOUP = "Spicy Soup" + SPIDER_ROLL = "Spider Roll" + SWAMP_CAKE = "Swamp Cake" + TASTY_CAKE = "Tasty Cake" + TASTY_ROLL = "Tasty Roll" + TOUGH_CAKE = "Tough Cake" + TURTLE_SOUP = "Turtle Soup" + VEDHA_SEA_CRISP = "Vedha Sea Crisp" + VEGGIE_CAKE = "Veggie Cake" + VEGGIE_ICE_CREAM = "Veggie Ice Cream" + VEGGIE_SOUP = "Veggie Soup" + VOLCANO_ROLL = "Volcano Roll" + HEALTH_UPGRADE = "Health Upgrade" + WOK = "Wok" + EEL_OIL_X_2 = "Eel Oil x 2" + FISH_MEAT_X_2 = "Fish Meat x 2" + FISH_OIL_X_3 = "Fish Oil x 3" + GLOWING_EGG_X_2 = "Glowing Egg x 2" + HEALING_POULTICE_X_2 = "Healing Poultice x 2" + HOT_SOUP_X_2 = "Hot Soup x 2" + LEADERSHIP_ROLL_X_2 = "Leadership Roll x 2" + LEAF_POULTICE_X_3 = "Leaf Poultice x 3" + PLANT_LEAF_X_2 = "Plant Leaf x 2" + PLANT_LEAF_X_3 = "Plant Leaf x 3" + ROTTEN_MEAT_X_2 = "Rotten Meat x 2" + ROTTEN_MEAT_X_8 = "Rotten Meat x 8" + SEA_LOAF_X_2 = "Sea Loaf x 2" + SMALL_BONE_X_3 = "Small Bone x 3" + SMALL_EGG_X_2 = "Small Egg x 2" + LI_AND_LI_SONG = "Li and Li Song" + SHIELD_SONG = "Shield Song" + BEAST_FORM = "Beast Form" + SUN_FORM = "Sun Form" + NATURE_FORM = "Nature Form" + ENERGY_FORM = "Energy Form" + BIND_SONG = "Bind Song" + FISH_FORM = "Fish Form" + SPIRIT_FORM = "Spirit Form" + DUAL_FORM = "Dual Form" + TRANSTURTLE_VEIL_TOP_LEFT = "Transturtle Veil top left" + TRANSTURTLE_VEIL_TOP_RIGHT = "Transturtle Veil top right" + TRANSTURTLE_OPEN_WATERS = "Transturtle Open Waters top right" + TRANSTURTLE_KELP_FOREST = "Transturtle Kelp Forest bottom left" + TRANSTURTLE_HOME_WATERS = "Transturtle Home Waters" + TRANSTURTLE_ABYSS = "Transturtle Abyss right" + TRANSTURTLE_BODY = "Transturtle Final Boss" + TRANSTURTLE_SIMON_SAYS = "Transturtle Simon Says" + TRANSTURTLE_ARNASSI_RUINS = "Transturtle Arnassi Ruins" + # Events name + BODY_TONGUE_CLEARED = "Body Tongue cleared" + HAS_SUN_CRYSTAL = "Has Sun Crystal" + FALLEN_GOD_BEATED = "Fallen God beated" + MITHALAN_GOD_BEATED = "Mithalan God beated" + DRUNIAN_GOD_BEATED = "Drunian God beated" + LUMEREAN_GOD_BEATED = "Lumerean God beated" + THE_GOLEM_BEATED = "The Golem beated" + NAUTILUS_PRIME_BEATED = "Nautilus Prime beated" + BLASTER_PEG_PRIME_BEATED = "Blaster Peg Prime beated" + MERGOG_BEATED = "Mergog beated" + MITHALAN_PRIESTS_BEATED = "Mithalan priests beated" + OCTOPUS_PRIME_BEATED = "Octopus Prime beated" + CRABBIUS_MAXIMUS_BEATED = "Crabbius Maximus beated" + MANTIS_SHRIMP_PRIME_BEATED = "Mantis Shrimp Prime beated" + KING_JELLYFISH_GOD_PRIME_BEATED = "King Jellyfish God Prime beated" + VICTORY = "Victory" + FIRST_SECRET_OBTAINED = "First Secret obtained" + SECOND_SECRET_OBTAINED = "Second Secret obtained" + THIRD_SECRET_OBTAINED = "Third Secret obtained" """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.PROGRESSION, 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 + ItemNames.ANEMONE: ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone + ItemNames.ARNASSI_STATUE: ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue + ItemNames.BIG_SEED: ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed + ItemNames.GLOWING_SEED: ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed + ItemNames.BLACK_PEARL: ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl + ItemNames.BABY_BLASTER: ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster + ItemNames.CRAB_ARMOR: ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume + ItemNames.BABY_DUMBO: ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo + ItemNames.TOOTH: ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss + ItemNames.ENERGY_STATUE: ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue + ItemNames.KROTITE_ARMOR: ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple + ItemNames.GOLDEN_STARFISH: ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star + ItemNames.GOLDEN_GEAR: ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear + ItemNames.JELLY_BEACON: ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon + ItemNames.JELLY_COSTUME: ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume + ItemNames.JELLY_PLANT: ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant + ItemNames.MITHALAS_DOLL: ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll + ItemNames.MITHALAN_DRESS: ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume + ItemNames.MITHALAS_BANNER: ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner + ItemNames.MITHALAS_POT: ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot + ItemNames.MUTANT_COSTUME: ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume + ItemNames.BABY_NAUTILUS: ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus + ItemNames.BABY_PIRANHA: ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha + ItemNames.ARNASSI_ARMOR: ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume + ItemNames.SEED_BAG: ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag + ItemNames.KING_S_SKULL: ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull + ItemNames.SONG_PLANT_SPORE: ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed + ItemNames.STONE_HEAD: ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head + ItemNames.SUN_KEY: ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key + ItemNames.GIRL_COSTUME: ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume + ItemNames.ODD_CONTAINER: ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest + ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head + ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg + ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed + ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume + ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker + ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All + ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi + ItemNames.ARCANE_POULTICE: ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice + ItemNames.BERRY_ICE_CREAM: ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream + ItemNames.BUTTERY_SEA_LOAF: ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf + ItemNames.COLD_BORSCHT: ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht + ItemNames.COLD_SOUP: ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup + ItemNames.CRAB_CAKE: ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake + ItemNames.DIVINE_SOUP: ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup + ItemNames.DUMBO_ICE_CREAM: ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream + ItemNames.FISH_OIL: ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil + ItemNames.GLOWING_EGG: ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg + ItemNames.HAND_ROLL: ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll + ItemNames.HEALING_POULTICE: ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice + ItemNames.HEARTY_SOUP: ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup + ItemNames.HOT_BORSCHT: ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht + ItemNames.HOT_SOUP: ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup + ItemNames.ICE_CREAM: ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream + ItemNames.LEADERSHIP_ROLL: ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll + ItemNames.LEAF_POULTICE: ItemData(698055, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leafpoultice + ItemNames.LEECHING_POULTICE: ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice + ItemNames.LEGENDARY_CAKE: ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake + ItemNames.LOAF_OF_LIFE: ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife + ItemNames.LONG_LIFE_SOUP: ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup + ItemNames.MAGIC_SOUP: ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup + ItemNames.MUSHROOM_X_2: ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom + ItemNames.PEROGI: ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi + ItemNames.PLANT_LEAF: ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf + ItemNames.PLUMP_PEROGI: ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi + ItemNames.POISON_LOAF: ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf + ItemNames.POISON_SOUP: ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup + ItemNames.RAINBOW_MUSHROOM: ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom + ItemNames.RAINBOW_SOUP: ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup + ItemNames.RED_BERRY: ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry + ItemNames.RED_BULB_X_2: ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb + ItemNames.ROTTEN_CAKE: ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake + ItemNames.ROTTEN_LOAF_X_8: ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf + ItemNames.ROTTEN_MEAT: ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat + ItemNames.ROYAL_SOUP: ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup + ItemNames.SEA_CAKE: ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake + ItemNames.SEA_LOAF: ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf + ItemNames.SHARK_FIN_SOUP: ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup + ItemNames.SIGHT_POULTICE: ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice + ItemNames.SMALL_BONE_X_2: ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone + ItemNames.SMALL_EGG: ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg + ItemNames.SMALL_TENTACLE_X_2: ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle + ItemNames.SPECIAL_BULB: ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb + ItemNames.SPECIAL_CAKE: ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake + ItemNames.SPICY_MEAT_X_2: ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat + ItemNames.SPICY_ROLL: ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll + ItemNames.SPICY_SOUP: ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup + ItemNames.SPIDER_ROLL: ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll + ItemNames.SWAMP_CAKE: ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake + ItemNames.TASTY_CAKE: ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake + ItemNames.TASTY_ROLL: ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll + ItemNames.TOUGH_CAKE: ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake + ItemNames.TURTLE_SOUP: ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup + ItemNames.VEDHA_SEA_CRISP: ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp + ItemNames.VEGGIE_CAKE: ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake + ItemNames.VEGGIE_ICE_CREAM: ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream + ItemNames.VEGGIE_SOUP: ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup + ItemNames.VOLCANO_ROLL: ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll + ItemNames.HEALTH_UPGRADE: ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_? + ItemNames.WOK: ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok + ItemNames.EEL_OIL_X_2: ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil + ItemNames.FISH_MEAT_X_2: ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat + ItemNames.FISH_OIL_X_3: ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil + ItemNames.GLOWING_EGG_X_2: ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg + ItemNames.HEALING_POULTICE_X_2: ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice + ItemNames.HOT_SOUP_X_2: ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup + ItemNames.LEADERSHIP_ROLL_X_2: ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll + ItemNames.LEAF_POULTICE_X_3: ItemData(698107, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leafpoultice + ItemNames.PLANT_LEAF_X_2: ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf + ItemNames.PLANT_LEAF_X_3: ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf + ItemNames.ROTTEN_MEAT_X_2: ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat + ItemNames.ROTTEN_MEAT_X_8: ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat + ItemNames.SEA_LOAF_X_2: ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf + ItemNames.SMALL_BONE_X_3: ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone + ItemNames.SMALL_EGG_X_2: ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg + ItemNames.LI_AND_LI_SONG: ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li + ItemNames.SHIELD_SONG: ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield + ItemNames.BEAST_FORM: ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast + ItemNames.SUN_FORM: ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun + ItemNames.NATURE_FORM: ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature + ItemNames.ENERGY_FORM: ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy + ItemNames.BIND_SONG: ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind + ItemNames.FISH_FORM: ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish + ItemNames.SPIRIT_FORM: ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit + ItemNames.DUAL_FORM: ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual + ItemNames.TRANSTURTLE_VEIL_TOP_LEFT: ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01 + ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT: ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02 + ItemNames.TRANSTURTLE_OPEN_WATERS: ItemData(698127, 1, ItemType.PROGRESSION, + ItemGroup.TURTLE), # transport_openwater03 + ItemNames.TRANSTURTLE_KELP_FOREST: ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), + # transport_forest04 + ItemNames.TRANSTURTLE_HOME_WATERS: ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea + ItemNames.TRANSTURTLE_ABYSS: ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03 + ItemNames.TRANSTURTLE_BODY: ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss + ItemNames.TRANSTURTLE_SIMON_SAYS: ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 + ItemNames.TRANSTURTLE_ARNASSI_RUINS: ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse } diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index f6e098103fdc..832d22f4ac87 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -26,476 +26,785 @@ def __init__(self, player: int, name="", code=None, parent=None) -> None: self.event = code is None -class AquariaLocations: +class AquariaLocationNames: + """ + Constants used to represent every name of every locations. + """ + + VERSE_CAVE_RIGHT_AREA_BULB_IN_THE_SKELETON_ROOM = "Verse Cave right area, bulb in the skeleton room" + VERSE_CAVE_RIGHT_AREA_BULB_IN_THE_PATH_RIGHT_OF_THE_SKELETON_ROOM = \ + "Verse Cave right area, bulb in the path right of the skeleton room" + VERSE_CAVE_RIGHT_AREA_BIG_SEED = "Verse Cave right area, Big Seed" + VERSE_CAVE_LEFT_AREA_THE_NAIJA_HINT_ABOUT_THE_SHIELD_ABILITY = \ + "Verse Cave left area, the Naija hint about the shield ability" + VERSE_CAVE_LEFT_AREA_BULB_IN_THE_CENTER_PART = "Verse Cave left area, bulb in the center part" + VERSE_CAVE_LEFT_AREA_BULB_IN_THE_RIGHT_PART = "Verse Cave left area, bulb in the right part" + VERSE_CAVE_LEFT_AREA_BULB_UNDER_THE_ROCK_AT_THE_END_OF_THE_PATH = \ + "Verse Cave left area, bulb under the rock at the end of the path" + HOME_WATERS_BULB_BELOW_THE_GROUPER_FISH = "Home Waters, bulb below the grouper fish" + HOME_WATERS_BULB_IN_THE_LITTLE_ROOM_ABOVE_THE_GROUPER_FISH = \ + "Home Waters, bulb in the little room above the grouper fish" + HOME_WATERS_BULB_IN_THE_END_OF_THE_PATH_CLOSE_TO_THE_VERSE_CAVE = \ + "Home Waters, bulb in the end of the path close to the Verse Cave" + HOME_WATERS_BULB_IN_THE_TOP_LEFT_PATH = "Home Waters, bulb in the top left path" + HOME_WATERS_BULB_CLOSE_TO_NAIJA_S_HOME = "Home Waters, bulb close to Naija's Home" + HOME_WATERS_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH_FROM_THE_VERSE_CAVE = \ + "Home Waters, bulb under the rock in the left path from the Verse Cave" + HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME = "Home Waters, bulb in the path below Nautilus Prime" + HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM = "Home Waters, bulb in the bottom left room" + HOME_WATERS_NAUTILUS_EGG = "Home Waters, Nautilus Egg" + HOME_WATERS_TRANSTURTLE = "Home Waters, Transturtle" + NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR = "Naija's Home, bulb after the energy door" + NAIJA_S_HOME_BULB_UNDER_THE_ROCK_AT_THE_RIGHT_OF_THE_MAIN_PATH = \ + "Naija's Home, bulb under the rock at the right of the main path" + SONG_CAVE_ERULIAN_SPIRIT = "Song Cave, Erulian spirit" + SONG_CAVE_BULB_IN_THE_TOP_RIGHT_PART = "Song Cave, bulb in the top right part" + SONG_CAVE_BULB_IN_THE_BIG_ANEMONE_ROOM = "Song Cave, bulb in the big anemone room" + SONG_CAVE_BULB_IN_THE_PATH_TO_THE_SINGING_STATUES = "Song Cave, bulb in the path to the singing statues" + SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES = \ + "Song Cave, bulb under the rock in the path to the singing statues" + SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR = "Song Cave, bulb under the rock close to the song door" + SONG_CAVE_VERSE_EGG = "Song Cave, Verse Egg" + SONG_CAVE_JELLY_BEACON = "Song Cave, Jelly Beacon" + SONG_CAVE_ANEMONE_SEED = "Song Cave, Anemone Seed" + ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE = "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, bulb in the bottom room blocked by a rock" + ENERGY_TEMPLE_ENERGY_IDOL = "Energy Temple, Energy Idol" + ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK = "Energy Temple second area, bulb under the rock" + ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR = "Energy Temple bottom entrance, Krotite Armor" + ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH = "Energy Temple third area, bulb in the bottom path" + ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH = "Energy Temple boss area, Fallen God Tooth" + ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG = "Energy Temple blaster room, Blaster Egg" + OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH = \ + "Open Waters top left area, bulb under the rock in the right path" + OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH = \ + "Open Waters top left area, bulb under the rock in the left path" + OPEN_WATERS_TOP_LEFT_AREA_BULB_TO_THE_RIGHT_OF_THE_SAVE_CRYSTAL = \ + "Open Waters top left area, bulb to the right of the save crystal" + OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS = \ + "Open Waters top right area, bulb in the small path before Mithalas" + OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_PATH_FROM_THE_LEFT_ENTRANCE = \ + "Open Waters top right area, bulb in the path from the left entrance" + OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_CLEARING_CLOSE_TO_THE_BOTTOM_EXIT = \ + "Open Waters top right area, bulb in the clearing close to the bottom exit" + OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_CLOSE_TO_THE_SAVE_CRYSTAL = \ + "Open Waters top right area, bulb in the big clearing close to the save crystal" + OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_TO_THE_TOP_EXIT = \ + "Open Waters top right area, bulb in the big clearing to the top exit" + OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_TURTLE_ROOM = "Open Waters top right area, bulb in the turtle room" + OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE = "Open Waters top right area, Transturtle" + OPEN_WATERS_TOP_RIGHT_AREA_FIRST_URN_IN_THE_MITHALAS_EXIT = \ + "Open Waters top right area, first urn in the Mithalas exit" + OPEN_WATERS_TOP_RIGHT_AREA_SECOND_URN_IN_THE_MITHALAS_EXIT = \ + "Open Waters top right area, second urn in the Mithalas exit" + OPEN_WATERS_TOP_RIGHT_AREA_THIRD_URN_IN_THE_MITHALAS_EXIT = \ + "Open Waters top right area, third urn in the Mithalas exit" + OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_BEHIND_THE_CHOMPER_FISH = \ + "Open Waters bottom left area, bulb behind the chomper fish" + OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS = \ + "Open Waters bottom left area, bulb inside the lowest fish pass" + OPEN_WATERS_SKELETON_PATH_BULB_CLOSE_TO_THE_RIGHT_EXIT = "Open Waters skeleton path, bulb close to the right exit" + OPEN_WATERS_SKELETON_PATH_BULB_BEHIND_THE_CHOMPER_FISH = "Open Waters skeleton path, bulb behind the chomper fish" + OPEN_WATERS_SKELETON_PATH_KING_SKULL = "Open Waters skeleton path, King Skull" + ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART = "Arnassi Ruins, bulb in the right part" + ARNASSI_RUINS_BULB_IN_THE_LEFT_PART = "Arnassi Ruins, bulb in the left part" + ARNASSI_RUINS_BULB_IN_THE_CENTER_PART = "Arnassi Ruins, bulb in the center part" + ARNASSI_RUINS_SONG_PLANT_SPORE = "Arnassi Ruins, Song Plant Spore" + ARNASSI_RUINS_ARNASSI_ARMOR = "Arnassi Ruins, Arnassi Armor" + ARNASSI_RUINS_ARNASSI_STATUE = "Arnassi Ruins, Arnassi Statue" + ARNASSI_RUINS_TRANSTURTLE = "Arnassi Ruins, Transturtle" + ARNASSI_RUINS_CRAB_ARMOR = "Arnassi Ruins, Crab Armor" + SIMON_SAYS_AREA_BEATING_SIMON_SAYS = "Simon Says area, beating Simon Says" + SIMON_SAYS_AREA_TRANSTURTLE = "Simon Says area, Transturtle" + MITHALAS_CITY_FIRST_BULB_IN_THE_LEFT_CITY_PART = "Mithalas City, first bulb in the left city part" + MITHALAS_CITY_SECOND_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 in the right part" + MITHALAS_CITY_BULB_AT_THE_TOP_OF_THE_CITY = "Mithalas City, bulb at the top of the city" + MITHALAS_CITY_FIRST_BULB_IN_A_BROKEN_HOME = "Mithalas City, first bulb in a broken home" + MITHALAS_CITY_SECOND_BULB_IN_A_BROKEN_HOME = "Mithalas City, second bulb in a broken home" + MITHALAS_CITY_BULB_IN_THE_BOTTOM_LEFT_PART = "Mithalas City, bulb in the bottom left part" + MITHALAS_CITY_FIRST_BULB_IN_ONE_OF_THE_HOMES = "Mithalas City, first bulb in one of the homes" + MITHALAS_CITY_SECOND_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, first urn in one of the homes" + MITHALAS_CITY_SECOND_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, first urn in the city reserve" + MITHALAS_CITY_SECOND_URN_IN_THE_CITY_RESERVE = "Mithalas City, second urn in the city reserve" + MITHALAS_CITY_THIRD_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, first bulb at the end of the top path" + MITHALAS_CITY_SECOND_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, bulb in the top path" + MITHALAS_CITY_MITHALAS_POT = "Mithalas City, Mithalas Pot" + MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE = "Mithalas City, urn in the Castle flower tube entrance" + MITHALAS_CITY_DOLL = "Mithalas City, Doll" + MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS = "Mithalas City, urn inside a home fish pass" + MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE = "Mithalas City Castle, bulb in the flesh hole" + MITHALAS_CITY_CASTLE_BLUE_BANNER = "Mithalas City Castle, Blue Banner" + MITHALAS_CITY_CASTLE_URN_IN_THE_BEDROOM = "Mithalas City Castle, urn in the bedroom" + MITHALAS_CITY_CASTLE_FIRST_URN_OF_THE_SINGLE_LAMP_PATH = "Mithalas City Castle, first urn of the single lamp path" + MITHALAS_CITY_CASTLE_SECOND_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, urn in the bottom room" + MITHALAS_CITY_CASTLE_FIRST_URN_ON_THE_ENTRANCE_PATH = "Mithalas City Castle, first urn on the entrance path" + MITHALAS_CITY_CASTLE_SECOND_URN_ON_THE_ENTRANCE_PATH = "Mithalas City Castle, second urn on the entrance path" + MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS = "Mithalas City Castle, beating the Priests" + MITHALAS_CITY_CASTLE_TRIDENT_HEAD = "Mithalas City Castle, Trident Head" + MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS = "Mithalas Cathedral, bulb in the flesh room with fleas" + MITHALAS_CATHEDRAL_MITHALAN_DRESS = "Mithalas Cathedral, Mithalan Dress" + MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_TOP_RIGHT_ROOM = "Mithalas Cathedral, first urn in the top right room" + MITHALAS_CATHEDRAL_SECOND_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, third urn in the top right room" + MITHALAS_CATHEDRAL_URN_BEHIND_THE_FLESH_VEIN = "Mithalas Cathedral, urn behind the flesh vein" + MITHALAS_CATHEDRAL_URN_IN_THE_TOP_LEFT_EYES_BOSS_ROOM = "Mithalas Cathedral, urn in the top left eyes boss room" + MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN =\ + "Mithalas Cathedral, first urn in the path behind the flesh vein" + MITHALAS_CATHEDRAL_SECOND_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, third urn in the path behind the flesh vein" + MITHALAS_CATHEDRAL_FOURTH_URN_IN_THE_TOP_RIGHT_ROOM = "Mithalas Cathedral, fourth urn in the top right room" + MITHALAS_CATHEDRAL_URN_BELOW_THE_LEFT_ENTRANCE = "Mithalas Cathedral, urn below the left entrance" + MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_BOTTOM_RIGHT_PATH = "Mithalas Cathedral, first urn in the bottom right path" + MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_BOTTOM_RIGHT_PATH = "Mithalas Cathedral, second urn in the bottom right path" + CATHEDRAL_UNDERGROUND_BULB_IN_THE_CENTER_PART = "Cathedral Underground, bulb in the center part" + CATHEDRAL_UNDERGROUND_FIRST_BULB_IN_THE_TOP_LEFT_PART = "Cathedral Underground, first bulb in the top left part" + CATHEDRAL_UNDERGROUND_SECOND_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, third bulb in the top left part" + CATHEDRAL_UNDERGROUND_BULB_CLOSE_TO_THE_SAVE_CRYSTAL = "Cathedral Underground, bulb close to the save crystal" + CATHEDRAL_UNDERGROUND_BULB_IN_THE_BOTTOM_RIGHT_PATH = "Cathedral Underground, bulb in the bottom right path" + MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD = "Mithalas boss area, beating Mithalan God" + KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING =\ + "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 path down from the top left clearing" + KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING = "Kelp Forest top left area, bulb in the top left clearing" + KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG = "Kelp Forest top left area, Jelly Egg" + KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG = "Kelp Forest top left area, bulb close to the Verse Egg" + KELP_FOREST_TOP_LEFT_AREA_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 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 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 big room" + KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_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, bulb at the top of the center clearing" + KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL = "Kelp Forest top right area, Black Pearl" + KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS = "Kelp Forest top right area, bulb in the top fish pass" + KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE = "Kelp Forest bottom left area, Transturtle" + KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS =\ + "Kelp Forest bottom left area, bulb close to the spirit crystals" + KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY = "Kelp Forest bottom left area, Walker Baby" + KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE = "Kelp Forest bottom left area, Fish Cave puzzle" + KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER = "Kelp Forest bottom right area, Odd Container" + KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD = "Kelp Forest boss area, beating Drunian God" + KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA = "Kelp Forest boss room, bulb at the bottom of the area" + KELP_FOREST_SPRITE_CAVE_BULB_INSIDE_THE_FISH_PASS = "Kelp Forest sprite cave, bulb inside the fish pass" + KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM = "Kelp Forest sprite cave, bulb in the second room" + KELP_FOREST_SPRITE_CAVE_SEED_BAG = "Kelp Forest sprite cave, Seed Bag" + MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE = "Mermog cave, bulb in the left part of the cave" + MERMOG_CAVE_PIRANHA_EGG = "Mermog cave, Piranha Egg" + THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE = "The Veil top left area, In Li's cave" + THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH =\ + "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, bulb hidden behind the blocking rock" + THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE = "The Veil top left area, Transturtle" + THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS = "The Veil top left area, bulb inside the fish pass" + TURTLE_CAVE_TURTLE_EGG = "Turtle cave, Turtle Egg" + TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF = "Turtle cave, bulb in Bubble Cliff" + TURTLE_CAVE_URCHIN_COSTUME = "Turtle cave, Urchin Costume" + THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF = \ + "The Veil top right area, bulb in the middle of the wall jump cliff" + THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH = "The Veil top right area, Golden Starfish" + THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL = \ + "The Veil top right area, bulb at the top of the waterfall" + THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE = "The Veil top right area, Transturtle" + THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH = "The Veil bottom area, bulb in the left path" + THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH = "The Veil bottom area, bulb in the spirit path" + THE_VEIL_BOTTOM_AREA_VERSE_EGG = "The Veil bottom area, Verse Egg" + THE_VEIL_BOTTOM_AREA_STONE_HEAD = "The Veil bottom area, Stone Head" + OCTOPUS_CAVE_DUMBO_EGG = "Octopus Cave, Dumbo Egg" + OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH =\ + "Octopus Cave, bulb in the path below the Octopus Cave path" + SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART = "Sun Temple, bulb in the top left part" + SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART = "Sun Temple, bulb in the top right part" + SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM = "Sun Temple, bulb at the top of the high dark room" + SUN_TEMPLE_GOLDEN_GEAR = "Sun Temple, Golden Gear" + SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE = "Sun Temple, first bulb of the temple" + SUN_TEMPLE_BULB_ON_THE_RIGHT_PART = "Sun Temple, bulb on the right part" + SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART = "Sun Temple, bulb in the hidden room of the right part" + SUN_TEMPLE_SUN_KEY = "Sun Temple, Sun Key" + SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB = "Sun Temple boss path, first path bulb" + SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB = "Sun Temple boss path, second path bulb" + SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB = "Sun Temple boss path, first cliff bulb" + SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB = "Sun Temple boss path, second cliff bulb" + SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD = "Sun Temple boss area, beating Lumerean God" + ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM = "Abyss left area, bulb in hidden path room" + ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART = "Abyss left area, bulb in the right part" + ABYSS_LEFT_AREA_GLOWING_SEED = "Abyss left area, Glowing Seed" + ABYSS_LEFT_AREA_GLOWING_PLANT = "Abyss left area, Glowing Plant" + ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS = "Abyss left area, bulb in the bottom fish pass" + ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH = "Abyss right area, bulb in the middle path" + ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_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, bulb in the left green room" + ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM = "Abyss right area, bulb behind the rock in the whale room" + ABYSS_RIGHT_AREA_TRANSTURTLE = "Abyss right area, Transturtle" + ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT = "Ice Cavern, bulb in the room to the right" + ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM = "Ice Cavern, first bulb in the top exit room" + ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM = "Ice Cavern, second bulb in the top exit room" + ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM = "Ice Cavern, third bulb in the top exit room" + ICE_CAVERN_BULB_IN_THE_LEFT_ROOM = "Ice Cavern, bulb in the left room" + BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL = "Bubble Cave, bulb in the left cave wall" + BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL =\ + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)" + BUBBLE_CAVE_VERSE_EGG = "Bubble Cave, Verse Egg" + KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY =\ + "King Jellyfish Cave, bulb in the right path from King Jelly" + KING_JELLYFISH_CAVE_JELLYFISH_COSTUME = "King Jellyfish Cave, Jellyfish Costume" + THE_WHALE_VERSE_EGG = "The Whale, Verse Egg" + SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL = "Sunken City right area, crate close to the save crystal" + SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM = "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 in the little pipe room" + SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL = "Sunken City left area, crate close to the save crystal" + SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM = "Sunken City left area, crate before the bedroom" + SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME = "Sunken City left area, Girl Costume" + SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA = "Sunken City, bulb on top of the boss area" + THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE = "The Body center area, breaking Li's cage" + THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE = \ + "The Body center area, bulb on the main path blocking tube" + THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM = "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, second bulb in the top face room" + THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM = "The Body left area, bulb below the water stream" + THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM = \ + "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 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 face room" + THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_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 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 Jelly Zap room" + THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM = "The Body bottom area, bulb in the nautilus room" + THE_BODY_BOTTOM_AREA_MUTANT_COSTUME = "The Body bottom area, Mutant Costume" + FINAL_BOSS_AREA_FIRST_BULB_IN_THE_TURTLE_ROOM = "Final Boss area, first bulb in the turtle room" + FINAL_BOSS_AREA_SECOND_BULB_IN_THE_TURTLE_ROOM = "Final Boss area, second bulb in the turtle room" + FINAL_BOSS_AREA_THIRD_BULB_IN_THE_TURTLE_ROOM = "Final Boss area, third bulb in the turtle room" + FINAL_BOSS_AREA_TRANSTURTLE = "Final Boss area, Transturtle" + FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM = "Final Boss area, bulb in the boss third form room" + BEATING_FALLEN_GOD = "Beating Fallen God" + BEATING_MITHALAN_GOD = "Beating Mithalan God" + BEATING_DRUNIAN_GOD = "Beating Drunian God" + BEATING_LUMEREAN_GOD = "Beating Lumerean God" + BEATING_THE_GOLEM = "Beating the Golem" + BEATING_NAUTILUS_PRIME = "Beating Nautilus Prime" + BEATING_BLASTER_PEG_PRIME = "Beating Blaster Peg Prime" + BEATING_MERGOG = "Beating Mergog" + BEATING_MITHALAN_PRIESTS = "Beating Mithalan priests" + BEATING_OCTOPUS_PRIME = "Beating Octopus Prime" + BEATING_CRABBIUS_MAXIMUS = "Beating Crabbius Maximus" + BEATING_MANTIS_SHRIMP_PRIME = "Beating Mantis Shrimp Prime" + BEATING_KING_JELLYFISH_GOD_PRIME = "Beating King Jellyfish God Prime" + FIRST_SECRET = "First Secret" + SECOND_SECRET = "Second Secret" + THIRD_SECRET = "Third Secret" + SUNKEN_CITY_CLEARED = "Sunken City cleared" + SUN_CRYSTAL = "Sun Crystal" + OBJECTIVE_COMPLETE = "Objective complete" + +class AquariaLocations: locations_verse_cave_r = { - "Verse Cave, bulb in the skeleton room": 698107, - "Verse Cave, bulb in the path right of the skeleton room": 698108, - "Verse Cave right area, Big Seed": 698175, + AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BULB_IN_THE_SKELETON_ROOM: 698107, + AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BULB_IN_THE_PATH_RIGHT_OF_THE_SKELETON_ROOM: 698108, + AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED: 698175, } locations_verse_cave_l = { - "Verse Cave, the Naija hint about the 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, + AquariaLocationNames.VERSE_CAVE_LEFT_AREA_THE_NAIJA_HINT_ABOUT_THE_SHIELD_ABILITY: 698200, + AquariaLocationNames.VERSE_CAVE_LEFT_AREA_BULB_IN_THE_CENTER_PART: 698021, + AquariaLocationNames.VERSE_CAVE_LEFT_AREA_BULB_IN_THE_RIGHT_PART: 698022, + AquariaLocationNames.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 below Nautilus Prime": 698059, - "Home Water, bulb in the little room above the grouper fish": 698060, - "Home Water, bulb in the end of the path close to 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 Naija's Home": 698064, - "Home Water, bulb under the rock in the left path from the Verse Cave": 698065, + AquariaLocationNames.HOME_WATERS_BULB_BELOW_THE_GROUPER_FISH: 698058, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_LITTLE_ROOM_ABOVE_THE_GROUPER_FISH: 698060, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_END_OF_THE_PATH_CLOSE_TO_THE_VERSE_CAVE: 698061, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_TOP_LEFT_PATH: 698062, + AquariaLocationNames.HOME_WATERS_BULB_CLOSE_TO_NAIJA_S_HOME: 698064, + AquariaLocationNames.HOME_WATERS_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH_FROM_THE_VERSE_CAVE: 698065, + } + + locations_home_water_behind_rocks = { + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME: 698059, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM: 698063, } locations_home_water_nautilus = { - "Home Water, Nautilus Egg": 698194, + AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG: 698194, } locations_home_water_transturtle = { - "Home Water, Transturtle": 698213, + AquariaLocationNames.HOME_WATERS_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, + AquariaLocationNames.NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR: 698119, + AquariaLocationNames.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 right 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, + AquariaLocationNames.SONG_CAVE_ERULIAN_SPIRIT: 698206, + AquariaLocationNames.SONG_CAVE_BULB_IN_THE_TOP_RIGHT_PART: 698071, + AquariaLocationNames.SONG_CAVE_BULB_IN_THE_BIG_ANEMONE_ROOM: 698072, + AquariaLocationNames.SONG_CAVE_BULB_IN_THE_PATH_TO_THE_SINGING_STATUES: 698073, + AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES: 698074, + AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR: 698075, + AquariaLocationNames.SONG_CAVE_VERSE_EGG: 698160, + AquariaLocationNames.SONG_CAVE_JELLY_BEACON: 698178, + AquariaLocationNames.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, + AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE: 698205, + AquariaLocationNames.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, + AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL: 698170, } locations_energy_temple_2 = { - "Energy Temple second area, bulb under the rock": 698028, + AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK: 698028, + # This can be accessible via locations_energy_temple_altar too } locations_energy_temple_altar = { - "Energy Temple bottom entrance, Krotite Armor": 698163, + AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR: 698163, } locations_energy_temple_3 = { - "Energy Temple third area, bulb in the bottom path": 698029, + AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH: 698029, } locations_energy_temple_boss = { - "Energy Temple boss area, Fallen God Tooth": 698169, + AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH: 698169, } locations_energy_temple_blaster_room = { - "Energy Temple blaster room, Blaster Egg": 698195, + AquariaLocationNames.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 crystal": 698003, + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH: 698001, + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH: 698002, + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_TO_THE_RIGHT_OF_THE_SAVE_CRYSTAL: 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 crystal": 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, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS: 698004, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_PATH_FROM_THE_LEFT_ENTRANCE: 698005, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_CLEARING_CLOSE_TO_THE_BOTTOM_EXIT: 698006, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_CLOSE_TO_THE_SAVE_CRYSTAL: 698007, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_TO_THE_TOP_EXIT: 698008, } locations_openwater_tr_turtle = { - "Open Water top right area, bulb in the turtle room": 698009, - "Open Water top right area, Transturtle": 698211, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_TURTLE_ROOM: 698009, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE: 698211, + } + + locations_openwater_tr_urns = { + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_FIRST_URN_IN_THE_MITHALAS_EXIT: 698148, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_SECOND_URN_IN_THE_MITHALAS_EXIT: 698149, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_THIRD_URN_IN_THE_MITHALAS_EXIT: 698150, } locations_openwater_bl = { - "Open Water bottom left area, bulb behind the chomper fish": 698011, - "Open Water bottom left area, bulb inside the lowest fish pass": 698010, + AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_BEHIND_THE_CHOMPER_FISH: 698011, + AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_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, + AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_CLOSE_TO_THE_RIGHT_EXIT: 698012, + AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_BEHIND_THE_CHOMPER_FISH: 698013, } locations_skeleton_path_sc = { - "Open Water skeleton path, King Skull": 698177, + AquariaLocationNames.OPEN_WATERS_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": 698179, - "Arnassi Ruins, Arnassi Armor": 698191, + AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART: 698014, + AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_LEFT_PART: 698015, + AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_CENTER_PART: 698016, + AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE: 698179, + AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR: 698191, } - locations_arnassi_path = { - "Arnassi Ruins, Arnassi Statue": 698164, + locations_arnassi_cave = { + AquariaLocationNames.ARNASSI_RUINS_ARNASSI_STATUE: 698164, } locations_arnassi_cave_transturtle = { - "Arnassi Ruins, Transturtle": 698217, + AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE: 698217, } locations_arnassi_crab_boss = { - "Arnassi Ruins, Crab Armor": 698187, + AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR: 698187, } locations_simon = { - "Simon Says area, beating Simon Says": 698156, - "Simon Says area, Transturtle": 698216, + AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS: 698156, + AquariaLocationNames.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, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_THE_LEFT_CITY_PART: 698030, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_THE_LEFT_CITY_PART: 698035, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_RIGHT_PART: 698031, + AquariaLocationNames.MITHALAS_CITY_BULB_AT_THE_TOP_OF_THE_CITY: 698033, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_A_BROKEN_HOME: 698034, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_A_BROKEN_HOME: 698041, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_BOTTOM_LEFT_PART: 698037, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_ONE_OF_THE_HOMES: 698038, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_ONE_OF_THE_HOMES: 698039, + } + + locations_mithalas_city_urns = { + AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_ONE_OF_THE_HOMES: 698123, + AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_ONE_OF_THE_HOMES: 698124, + AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_THE_CITY_RESERVE: 698125, + AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_THE_CITY_RESERVE: 698126, + AquariaLocationNames.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 Castle flower tube entrance": 698128, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH: 698032, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH: 698040, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH: 698036, + AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT: 698174, + AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE: 698128, } locations_mithalas_city_fishpass = { - "Mithalas City, Doll": 698173, - "Mithalas City, urn inside a home fish pass": 698129, + AquariaLocationNames.MITHALAS_CITY_DOLL: 698173, + AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS: 698129, + } + + locations_mithalas_castle = { + AquariaLocationNames.MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE: 698042, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BLUE_BANNER: 698165, + } + + locations_mithalas_castle_urns = { + AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BEDROOM: 698130, + AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_OF_THE_SINGLE_LAMP_PATH: 698131, + AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_OF_THE_SINGLE_LAMP_PATH: 698132, + AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BOTTOM_ROOM: 698133, + AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_ON_THE_ENTRANCE_PATH: 698134, + AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_ON_THE_ENTRANCE_PATH: 698135, } - 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_mithalas_castle_tube = { + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS: 698208, } - locations_cathedral_l_tube = { - "Mithalas City Castle, beating the Priests": 698208, + locations_mithalas_castle_sc = { + AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD: 698183, } - locations_cathedral_l_sc = { - "Mithalas City Castle, Trident Head": 698183, + locations_cathedral_top_start = { + AquariaLocationNames.MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS: 698139, + AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS: 698189, } - 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, fourth urn in the top right room": 698147, - "Mithalas Cathedral, Mithalan Dress": 698189, - "Mithalas Cathedral, urn below the left entrance": 698198, + locations_cathedral_top_start_urns = { + AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_TOP_RIGHT_ROOM: 698136, + AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_TOP_RIGHT_ROOM: 698137, + AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_TOP_RIGHT_ROOM: 698138, + AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BEHIND_THE_FLESH_VEIN: 698142, + AquariaLocationNames.MITHALAS_CATHEDRAL_URN_IN_THE_TOP_LEFT_EYES_BOSS_ROOM: 698143, + AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN: 698144, + AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN: 698145, + AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN: 698146, + AquariaLocationNames.MITHALAS_CATHEDRAL_FOURTH_URN_IN_THE_TOP_RIGHT_ROOM: 698147, + AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BELOW_THE_LEFT_ENTRANCE: 698198, + } + + locations_cathedral_top_end = { + AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_BOTTOM_RIGHT_PATH: 698140, + AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_BOTTOM_RIGHT_PATH: 698141, } 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 crystal": 698117, - "Cathedral Underground, bulb in the bottom right path": 698118, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_CENTER_PART: 698113, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_FIRST_BULB_IN_THE_TOP_LEFT_PART: 698114, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_SECOND_BULB_IN_THE_TOP_LEFT_PART: 698115, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_THIRD_BULB_IN_THE_TOP_LEFT_PART: 698116, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_CLOSE_TO_THE_SAVE_CRYSTAL: 698117, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_BOTTOM_RIGHT_PATH: 698118, } locations_cathedral_boss = { - "Mithalas boss area, beating Mithalan God": 698202, + AquariaLocationNames.MITHALAS_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 area, Jelly Egg": 698185, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING: 698044, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_PATH_DOWN_FROM_THE_TOP_LEFT_CLEARING: 698045, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING: 698046, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_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_tl_verse_egg_room = { + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG: 698047, + AquariaLocationNames.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, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH: 698048, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_LEFT_OF_THE_CENTER_CLEARING: 698049, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_BIG_ROOM: 698051, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_ROOM: 698052, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_CENTER_CLEARING: 698053, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL: 698167, } locations_forest_tr_fp = { - "Kelp Forest top right area, bulb in the top fish pass": 698050, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS: 698050, } locations_forest_bl = { - "Kelp Forest bottom left area, Transturtle": 698212, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE: 698212, } locations_forest_bl_sc = { - "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054, - "Kelp Forest bottom left area, Walker Baby": 698186, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS: 698054, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY: 698186, + } + + locations_forest_fish_cave = { + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE: 698207, } locations_forest_br = { - "Kelp Forest bottom right area, Odd Container": 698168, + AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER: 698168, } locations_forest_boss = { - "Kelp Forest boss area, beating Drunian God": 698204, + AquariaLocationNames.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, + AquariaLocationNames.KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA: 698055, } - locations_forest_sprite_cave = { - "Kelp Forest sprite cave, bulb inside the fish pass": 698056, + locations_sprite_cave = { + AquariaLocationNames.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_sprite_cave_tube = { + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM: 698057, + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG: 698176, } locations_mermog_cave = { - "Mermog cave, bulb in the left part of the cave": 698121, + AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE: 698121, } locations_mermog_boss = { - "Mermog cave, Piranha Egg": 698197, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG: 698197, } locations_veil_tl = { - "The Veil top left area, In Li's 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, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE: 698199, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH: 698078, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK: 698076, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE: 698209, } locations_veil_tl_fp = { - "The Veil top left area, bulb inside the fish pass": 698077, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS: 698077, } locations_turtle_cave = { - "Turtle cave, Turtle Egg": 698184, + AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG: 698184, } locations_turtle_cave_bubble = { - "Turtle cave, bulb in Bubble Cliff": 698000, - "Turtle cave, Urchin Costume": 698193, + AquariaLocationNames.TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF: 698000, + AquariaLocationNames.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": 698180, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF: 698079, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH: 698180, } locations_veil_tr_l = { - "The Veil top right area, bulb at the top of the waterfall": 698080, - "The Veil top right area, Transturtle": 698210, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL: 698080, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE: 698210, } - locations_veil_bl = { - "The Veil bottom area, bulb in the left path": 698082, + locations_veil_b = { + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH: 698082, } locations_veil_b_sc = { - "The Veil bottom area, bulb in the spirit path": 698081, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH: 698081, } - locations_veil_bl_fp = { - "The Veil bottom area, Verse Egg": 698157, + locations_veil_b_fp = { + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG: 698157, } locations_veil_br = { - "The Veil bottom area, Stone Head": 698181, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_STONE_HEAD: 698181, } locations_octo_cave_t = { - "Octopus Cave, Dumbo Egg": 698196, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG: 698196, } locations_octo_cave_b = { - "Octopus Cave, bulb in the path below the Octopus Cave path": 698122, + AquariaLocationNames.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, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART: 698094, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART: 698095, + AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM: 698096, + AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR: 698171, } locations_sun_temple_r = { - "Sun Temple, first bulb of the temple": 698091, - "Sun Temple, bulb on the right part": 698092, - "Sun Temple, bulb in the hidden room of the right part": 698093, - "Sun Temple, Sun Key": 698182, + AquariaLocationNames.SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE: 698091, + AquariaLocationNames.SUN_TEMPLE_BULB_ON_THE_RIGHT_PART: 698092, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART: 698093, + AquariaLocationNames.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, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB: 698017, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB: 698018, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB: 698019, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB: 698020, } locations_sun_temple_boss = { - "Sun Temple boss area, beating Sun God": 698203, + AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_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, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM: 698024, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART: 698025, + AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED: 698166, + AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT: 698172, } locations_abyss_lb = { - "Abyss left area, bulb in the bottom fish pass": 698026, + AquariaLocationNames.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, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH: 698110, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH: 698111, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM: 698112, + } + + locations_abyss_r_whale = { + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM: 698109, } locations_abyss_r_transturtle = { - "Abyss right area, Transturtle": 698214, + AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE: 698214, } locations_ice_cave = { - "Ice Cave, bulb in the room to the right": 698083, - "Ice Cave, first bulb in the top exit room": 698084, - "Ice Cave, second bulb in the top exit room": 698085, - "Ice Cave, third bulb in the top exit room": 698086, - "Ice Cave, bulb in the left room": 698087, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT: 698083, + AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM: 698084, + AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM: 698085, + AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM: 698086, + AquariaLocationNames.ICE_CAVERN_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 crystal)": 698090, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL: 698089, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL: 698090, } locations_bubble_cave_boss = { - "Bubble Cave, Verse Egg": 698161, + AquariaLocationNames.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, + AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY: 698088, + AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME: 698188, } locations_whale = { - "The Whale, Verse Egg": 698159, + AquariaLocationNames.THE_WHALE_VERSE_EGG: 698159, } locations_sunken_city_r = { - "Sunken City right area, crate close to the save crystal": 698154, - "Sunken City right area, crate in the left bottom room": 698155, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL: 698154, + AquariaLocationNames.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 crystal": 698152, - "Sunken City left area, crate before the bedroom": 698153, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM: 698151, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL: 698152, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM: 698153, } locations_sunken_city_l_bedroom = { - "Sunken City left area, Girl Costume": 698192, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME: 698192, } locations_sunken_city_boss = { - "Sunken City, bulb on top of the boss area": 698043, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA: 698043, } locations_body_c = { - "The Body center area, breaking Li's cage": 698201, - "The Body center area, bulb on the main path blocking tube": 698097, + AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE: 698201, + AquariaLocationNames.THE_BODY_CENTER_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 below 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, + AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM: 698066, + AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM: 698069, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM: 698067, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM: 698068, + AquariaLocationNames.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, + AquariaLocationNames.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, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM: 698098, + AquariaLocationNames.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, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM: 698101, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM: 698102, + AquariaLocationNames.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 bulb in the turtle room": 698104, - "Final Boss area, third bulb in the turtle room": 698105, - "Final Boss area, Transturtle": 698215, + AquariaLocationNames.FINAL_BOSS_AREA_FIRST_BULB_IN_THE_TURTLE_ROOM: 698103, + AquariaLocationNames.FINAL_BOSS_AREA_SECOND_BULB_IN_THE_TURTLE_ROOM: 698104, + AquariaLocationNames.FINAL_BOSS_AREA_THIRD_BULB_IN_THE_TURTLE_ROOM: 698105, + AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE: 698215, } locations_final_boss = { - "Final Boss area, bulb in the boss third form room": 698106, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM: 698106, } @@ -503,11 +812,12 @@ class AquariaLocations: **AquariaLocations.locations_openwater_tl, **AquariaLocations.locations_openwater_tr, **AquariaLocations.locations_openwater_tr_turtle, + **AquariaLocations.locations_openwater_tr_urns, **AquariaLocations.locations_openwater_bl, **AquariaLocations.locations_skeleton_path, **AquariaLocations.locations_skeleton_path_sc, **AquariaLocations.locations_arnassi, - **AquariaLocations.locations_arnassi_path, + **AquariaLocations.locations_arnassi_cave, **AquariaLocations.locations_arnassi_cave_transturtle, **AquariaLocations.locations_arnassi_crab_boss, **AquariaLocations.locations_sun_temple_l, @@ -519,6 +829,7 @@ class AquariaLocations: **AquariaLocations.locations_abyss_l, **AquariaLocations.locations_abyss_lb, **AquariaLocations.locations_abyss_r, + **AquariaLocations.locations_abyss_r_whale, **AquariaLocations.locations_abyss_r_transturtle, **AquariaLocations.locations_energy_temple_1, **AquariaLocations.locations_energy_temple_2, @@ -528,16 +839,20 @@ class AquariaLocations: **AquariaLocations.locations_energy_temple_altar, **AquariaLocations.locations_energy_temple_idol, **AquariaLocations.locations_mithalas_city, + **AquariaLocations.locations_mithalas_city_urns, **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_mithalas_castle, + **AquariaLocations.locations_mithalas_castle_urns, + **AquariaLocations.locations_mithalas_castle_tube, + **AquariaLocations.locations_mithalas_castle_sc, + **AquariaLocations.locations_cathedral_top_start, + **AquariaLocations.locations_cathedral_top_start_urns, + **AquariaLocations.locations_cathedral_top_end, **AquariaLocations.locations_cathedral_underground, **AquariaLocations.locations_cathedral_boss, **AquariaLocations.locations_forest_tl, - **AquariaLocations.locations_forest_tl_fp, + **AquariaLocations.locations_forest_tl_verse_egg_room, **AquariaLocations.locations_forest_tr, **AquariaLocations.locations_forest_tr_fp, **AquariaLocations.locations_forest_bl, @@ -545,10 +860,11 @@ class AquariaLocations: **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_sprite_cave, + **AquariaLocations.locations_sprite_cave_tube, **AquariaLocations.locations_forest_fish_cave, **AquariaLocations.locations_home_water, + **AquariaLocations.locations_home_water_behind_rocks, **AquariaLocations.locations_home_water_transturtle, **AquariaLocations.locations_home_water_nautilus, **AquariaLocations.locations_body_l, @@ -565,9 +881,9 @@ class AquariaLocations: **AquariaLocations.locations_turtle_cave_bubble, **AquariaLocations.locations_veil_tr_r, **AquariaLocations.locations_veil_tr_l, - **AquariaLocations.locations_veil_bl, + **AquariaLocations.locations_veil_b, **AquariaLocations.locations_veil_b_sc, - **AquariaLocations.locations_veil_bl_fp, + **AquariaLocations.locations_veil_b_fp, **AquariaLocations.locations_veil_br, **AquariaLocations.locations_ice_cave, **AquariaLocations.locations_king_jellyfish_cave, diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py index 8c0142debff0..c73c108a9544 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -15,7 +15,10 @@ class IngredientRandomizer(Choice): """ display_name = "Randomize Ingredients" option_off = 0 + alias_false = 0 option_common_ingredients = 1 + alias_on = 1 + alias_true = 1 option_all_ingredients = 2 default = 0 @@ -29,14 +32,43 @@ class TurtleRandomizer(Choice): """Randomize the transportation turtle.""" display_name = "Turtle Randomizer" option_none = 0 + alias_off = 0 + alias_false = 0 option_all = 1 option_all_except_final = 2 + alias_on = 2 + alias_true = 2 default = 2 -class EarlyEnergyForm(DefaultOnToggle): - """ Force the Energy Form to be in a location early in the game """ - display_name = "Early Energy Form" +class EarlyBindSong(Choice): + """ + Force the Bind song to be in a location early in the multiworld (or directly in your world if Early and Local is + selected). + """ + display_name = "Early Bind song" + option_off = 0 + alias_false = 0 + option_early = 1 + alias_on = 1 + alias_true = 1 + option_early_and_local = 2 + default = 1 + + +class EarlyEnergyForm(Choice): + """ + Force the Energy form to be in a location early in the multiworld (or directly in your world if Early and Local is + selected). + """ + display_name = "Early Energy form" + option_off = 0 + alias_false = 0 + option_early = 1 + alias_on = 1 + alias_true = 1 + option_early_and_local = 2 + default = 1 class AquarianTranslation(Toggle): @@ -47,7 +79,7 @@ class AquarianTranslation(Toggle): class BigBossesToBeat(Range): """ The 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". + "Fallen God", "Mithalan God", "Drunian God", "Lumerean God" and "The Golem". """ display_name = "Big bosses to beat" range_start = 0 @@ -104,7 +136,7 @@ class LightNeededToGetToDarkPlaces(DefaultOnToggle): display_name = "Light needed to get to dark places" -class BindSongNeededToGetUnderRockBulb(Toggle): +class BindSongNeededToGetUnderRockBulb(DefaultOnToggle): """ Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks. """ @@ -121,13 +153,18 @@ class BlindGoal(Toggle): class UnconfineHomeWater(Choice): """ - Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song. + Open the way out of the Home Waters area so that Naija can go to open water and beyond without the bind song. + Note that if you turn this option off, it is recommended to turn on the Early Energy form and Early Bind Song + options. """ - display_name = "Unconfine Home Water Area" + display_name = "Unconfine Home Waters Area" option_off = 0 + alias_false = 0 option_via_energy_door = 1 option_via_transturtle = 2 option_via_both = 3 + alias_on = 3 + alias_true = 3 default = 0 @@ -142,6 +179,7 @@ class AquariaOptions(PerGameCommonOptions): big_bosses_to_beat: BigBossesToBeat turtle_randomizer: TurtleRandomizer early_energy_form: EarlyEnergyForm + early_bind_song: EarlyBindSong light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb unconfine_home_water: UnconfineHomeWater diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 7a41e0d0c864..40170e0c3262 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -5,10 +5,10 @@ """ from typing import Dict, Optional -from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, CollectionState -from .Items import AquariaItem -from .Locations import AquariaLocations, AquariaLocation -from .Options import AquariaOptions +from BaseClasses import MultiWorld, Region, Entrance, Item, ItemClassification, CollectionState +from .Items import AquariaItem, ItemNames +from .Locations import AquariaLocations, AquariaLocation, AquariaLocationNames +from .Options import AquariaOptions, UnconfineHomeWater from worlds.generic.Rules import add_rule, set_rule @@ -16,28 +16,28 @@ def _has_hot_soup(state: CollectionState, player: int) -> bool: """`player` in `state` has the hotsoup item""" - return state.has_any({"Hot soup", "Hot soup x 2"}, player) + return state.has_any({ItemNames.HOT_SOUP, ItemNames.HOT_SOUP_X_2}, player) def _has_tongue_cleared(state: CollectionState, player: int) -> bool: """`player` in `state` has the Body tongue cleared item""" - return state.has("Body tongue cleared", player) + return state.has(ItemNames.BODY_TONGUE_CLEARED, player) def _has_sun_crystal(state: CollectionState, player: int) -> bool: """`player` in `state` has the Sun crystal item""" - return state.has("Has sun crystal", player) and _has_bind_song(state, player) + return state.has(ItemNames.HAS_SUN_CRYSTAL, player) and _has_bind_song(state, player) def _has_li(state: CollectionState, player: int) -> bool: """`player` in `state` has Li in its team""" - return state.has("Li and Li song", player) + return state.has(ItemNames.LI_AND_LI_SONG, player) def _has_damaging_item(state: CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", - "Baby Piranha", "Baby Blaster"}, player) + return state.has_any({ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, ItemNames.LI_AND_LI_SONG, + ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, ItemNames.BABY_BLASTER}, player) def _has_energy_attack_item(state: CollectionState, player: int) -> bool: @@ -47,22 +47,22 @@ def _has_energy_attack_item(state: CollectionState, player: int) -> bool: def _has_shield_song(state: CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has("Shield song", player) + return state.has(ItemNames.SHIELD_SONG, player) def _has_bind_song(state: CollectionState, player: int) -> bool: """`player` in `state` has the bind song item""" - return state.has("Bind song", player) + return state.has(ItemNames.BIND_SONG, player) def _has_energy_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the energy form item""" - return state.has("Energy form", player) + return state.has(ItemNames.ENERGY_FORM, player) def _has_beast_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the beast form item""" - return state.has("Beast form", player) + return state.has(ItemNames.BEAST_FORM, player) def _has_beast_and_soup_form(state: CollectionState, player: int) -> bool: @@ -72,55 +72,61 @@ def _has_beast_and_soup_form(state: CollectionState, player: int) -> bool: def _has_beast_form_or_arnassi_armor(state: CollectionState, player: int) -> bool: """`player` in `state` has the beast form item""" - return _has_beast_form(state, player) or state.has("Arnassi Armor", player) + return _has_beast_form(state, player) or state.has(ItemNames.ARNASSI_ARMOR, player) def _has_nature_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the nature form item""" - return state.has("Nature form", player) + return state.has(ItemNames.NATURE_FORM, player) def _has_sun_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the sun form item""" - return state.has("Sun form", player) + return state.has(ItemNames.SUN_FORM, player) def _has_light(state: CollectionState, player: int) -> bool: """`player` in `state` has the light item""" - return state.has("Baby Dumbo", player) or _has_sun_form(state, player) + return state.has(ItemNames.BABY_DUMBO, player) or _has_sun_form(state, player) def _has_dual_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the dual form item""" - return _has_li(state, player) and state.has("Dual form", player) + return _has_li(state, player) and state.has(ItemNames.DUAL_FORM, player) def _has_fish_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the fish form item""" - return state.has("Fish form", player) + return state.has(ItemNames.FISH_FORM, player) def _has_spirit_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the spirit form item""" - return state.has("Spirit form", player) + return state.has(ItemNames.SPIRIT_FORM, player) def _has_big_bosses(state: CollectionState, player: int) -> bool: """`player` in `state` has beated every big bosses""" - return state.has_all({"Fallen God beated", "Mithalan God beated", "Drunian God beated", - "Sun God beated", "The Golem beated"}, player) + return state.has_all({ItemNames.FALLEN_GOD_BEATED, ItemNames.MITHALAN_GOD_BEATED, ItemNames.DRUNIAN_GOD_BEATED, + ItemNames.LUMEREAN_GOD_BEATED, ItemNames.THE_GOLEM_BEATED}, player) def _has_mini_bosses(state: CollectionState, player: int) -> bool: """`player` in `state` has beated every big bosses""" - return state.has_all({"Nautilus Prime beated", "Blaster Peg Prime beated", "Mergog beated", - "Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated", - "Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player) + return state.has_all({ItemNames.NAUTILUS_PRIME_BEATED, ItemNames.BLASTER_PEG_PRIME_BEATED, ItemNames.MERGOG_BEATED, + ItemNames.MITHALAN_PRIESTS_BEATED, ItemNames.OCTOPUS_PRIME_BEATED, + ItemNames.CRABBIUS_MAXIMUS_BEATED, ItemNames.MANTIS_SHRIMP_PRIME_BEATED, + ItemNames.KING_JELLYFISH_GOD_PRIME_BEATED}, player) def _has_secrets(state: CollectionState, player: int) -> bool: - return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"}, player) + """The secrets have been acquired in the `state` of the `player`""" + return state.has_all({ItemNames.FIRST_SECRET_OBTAINED, ItemNames.SECOND_SECRET_OBTAINED, + ItemNames.THIRD_SECRET_OBTAINED}, player) +def _item_not_advancement(item: Item): + """The `item` is not an advancement item""" + return not item.advancement class AquariaRegions: """ @@ -130,6 +136,7 @@ class AquariaRegions: verse_cave_r: Region verse_cave_l: Region home_water: Region + home_water_behind_rocks: Region home_water_nautilus: Region home_water_transturtle: Region naija_home: Region @@ -138,33 +145,40 @@ class AquariaRegions: energy_temple_2: Region energy_temple_3: Region energy_temple_boss: Region + energy_temple_4: Region energy_temple_idol: Region energy_temple_blaster_room: Region energy_temple_altar: Region openwater_tl: Region openwater_tr: Region openwater_tr_turtle: Region + openwater_tr_urns: Region openwater_bl: Region openwater_br: Region skeleton_path: Region skeleton_path_sc: Region arnassi: Region arnassi_cave_transturtle: Region - arnassi_path: Region + arnassi_cave: Region arnassi_crab_boss: Region simon: Region mithalas_city: Region + mithalas_city_urns: Region mithalas_city_top_path: Region mithalas_city_fishpass: Region - cathedral_l: Region - cathedral_l_tube: Region - cathedral_l_sc: Region - cathedral_r: Region + mithalas_castle: Region + mithalas_castle_urns: Region + mithalas_castle_tube: Region + mithalas_castle_sc: Region + cathedral_top: Region + cathedral_top_start: Region + cathedral_top_start_urns: Region + cathedral_top_end: Region cathedral_underground: Region cathedral_boss_l: Region cathedral_boss_r: Region forest_tl: Region - forest_tl_fp: Region + forest_tl_verse_egg_room: Region forest_tr: Region forest_tr_fp: Region forest_bl: Region @@ -172,24 +186,26 @@ class AquariaRegions: forest_br: Region forest_boss: Region forest_boss_entrance: Region - forest_sprite_cave: Region - forest_sprite_cave_tube: Region + sprite_cave: Region + sprite_cave_tube: Region mermog_cave: Region mermog_boss: Region forest_fish_cave: Region veil_tl: Region veil_tl_fp: Region veil_tr_l: Region + veil_tr_l_fp: Region veil_tr_r: Region - veil_bl: Region + veil_b: Region veil_b_sc: Region - veil_bl_fp: Region + veil_b_fp: Region veil_br: Region octo_cave_t: Region octo_cave_b: Region turtle_cave: Region turtle_cave_bubble: Region sun_temple_l: Region + sun_temple_l_entrance: Region sun_temple_r: Region sun_temple_boss_path: Region sun_temple_boss: Region @@ -198,13 +214,16 @@ class AquariaRegions: abyss_r: Region abyss_r_transturtle: Region ice_cave: Region + frozen_feil: Region bubble_cave: Region bubble_cave_boss: Region king_jellyfish_cave: Region + abyss_r_whale: Region whale: Region first_secret: Region sunken_city_l: Region - sunken_city_r: Region + sunken_city_l_crates: Region + sunken_city_r_crates: Region sunken_city_boss: Region sunken_city_l_bedroom: Region body_c: Region @@ -250,11 +269,13 @@ def __create_home_water_area(self) -> None: AquariaLocations.locations_verse_cave_r) self.verse_cave_l = self.__add_region("Verse Cave left area", AquariaLocations.locations_verse_cave_l) - self.home_water = self.__add_region("Home Water", AquariaLocations.locations_home_water) - self.home_water_nautilus = self.__add_region("Home Water, Nautilus nest", + self.home_water = self.__add_region("Home Waters", AquariaLocations.locations_home_water) + self.home_water_nautilus = self.__add_region("Home Waters, Nautilus nest", AquariaLocations.locations_home_water_nautilus) - self.home_water_transturtle = self.__add_region("Home Water, turtle room", + self.home_water_transturtle = self.__add_region("Home Waters, turtle room", AquariaLocations.locations_home_water_transturtle) + self.home_water_behind_rocks = self.__add_region("Home Waters, behind rock", + AquariaLocations.locations_home_water_behind_rocks) self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home) self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave) @@ -276,29 +297,32 @@ def __create_energy_temple(self) -> None: AquariaLocations.locations_energy_temple_idol) self.energy_temple_blaster_room = self.__add_region("Energy Temple blaster room", AquariaLocations.locations_energy_temple_blaster_room) + self.energy_temple_4 = self.__add_region("Energy Temple after boss path", None) def __create_openwater(self) -> None: """ Create the `openwater_*`, `skeleton_path`, `arnassi*` and `simon` regions """ - self.openwater_tl = self.__add_region("Open Water top left area", + self.openwater_tl = self.__add_region("Open Waters top left area", AquariaLocations.locations_openwater_tl) - self.openwater_tr = self.__add_region("Open Water top right area", + self.openwater_tr = self.__add_region("Open Waters top right area", AquariaLocations.locations_openwater_tr) - self.openwater_tr_turtle = self.__add_region("Open Water top right area, turtle room", + self.openwater_tr_turtle = self.__add_region("Open Waters top right area, turtle room", AquariaLocations.locations_openwater_tr_turtle) - self.openwater_bl = self.__add_region("Open Water bottom left area", + self.openwater_tr_urns = self.__add_region("Open Waters top right area, Mithalas entrance", + AquariaLocations.locations_openwater_tr_urns) + self.openwater_bl = self.__add_region("Open Waters bottom left area", AquariaLocations.locations_openwater_bl) - self.openwater_br = self.__add_region("Open Water bottom right area", None) - self.skeleton_path = self.__add_region("Open Water skeleton path", + self.openwater_br = self.__add_region("Open Waters bottom right area", None) + self.skeleton_path = self.__add_region("Open Waters skeleton path", AquariaLocations.locations_skeleton_path) - self.skeleton_path_sc = self.__add_region("Open Water skeleton path spirit crystal", + self.skeleton_path_sc = self.__add_region("Open Waters skeleton path spirit crystal", AquariaLocations.locations_skeleton_path_sc) self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi) - self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path", - AquariaLocations.locations_arnassi_path) - self.arnassi_cave_transturtle = self.__add_region("Arnassi Ruins, transturtle area", + self.arnassi_cave = self.__add_region("Arnassi Ruins cave", + AquariaLocations.locations_arnassi_cave) + self.arnassi_cave_transturtle = self.__add_region("Arnassi Ruins cave, transturtle area", AquariaLocations.locations_arnassi_cave_transturtle) self.arnassi_crab_boss = self.__add_region("Arnassi Ruins, Crabbius Maximus lair", AquariaLocations.locations_arnassi_crab_boss) @@ -309,22 +333,29 @@ def __create_mithalas(self) -> None: """ self.mithalas_city = self.__add_region("Mithalas City", AquariaLocations.locations_mithalas_city) + self.mithalas_city_urns = self.__add_region("Mithalas City urns", AquariaLocations.locations_mithalas_city_urns) self.mithalas_city_fishpass = self.__add_region("Mithalas City fish pass", AquariaLocations.locations_mithalas_city_fishpass) self.mithalas_city_top_path = self.__add_region("Mithalas City top path", AquariaLocations.locations_mithalas_city_top_path) - self.cathedral_l = self.__add_region("Mithalas castle", AquariaLocations.locations_cathedral_l) - self.cathedral_l_tube = self.__add_region("Mithalas castle, plant tube entrance", - AquariaLocations.locations_cathedral_l_tube) - self.cathedral_l_sc = self.__add_region("Mithalas castle spirit crystal", - AquariaLocations.locations_cathedral_l_sc) - self.cathedral_r = self.__add_region("Mithalas Cathedral", - AquariaLocations.locations_cathedral_r) + self.mithalas_castle = self.__add_region("Mithalas castle", AquariaLocations.locations_mithalas_castle) + self.mithalas_castle_urns = self.__add_region("Mithalas castle urns", + AquariaLocations.locations_mithalas_castle_urns) + self.mithalas_castle_tube = self.__add_region("Mithalas castle, plant tube entrance", + AquariaLocations.locations_mithalas_castle_tube) + self.mithalas_castle_sc = self.__add_region("Mithalas castle spirit crystal", + AquariaLocations.locations_mithalas_castle_sc) + self.cathedral_top_start = self.__add_region("Mithalas Cathedral start", + AquariaLocations.locations_cathedral_top_start) + self.cathedral_top_start_urns = self.__add_region("Mithalas Cathedral start urns", + AquariaLocations.locations_cathedral_top_start_urns) + self.cathedral_top_end = self.__add_region("Mithalas Cathedral end", + AquariaLocations.locations_cathedral_top_end) self.cathedral_underground = self.__add_region("Mithalas Cathedral underground", AquariaLocations.locations_cathedral_underground) - self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", None) - self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", + self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God", AquariaLocations.locations_cathedral_boss) + self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, before Mithalan God", None) def __create_forest(self) -> None: """ @@ -332,8 +363,8 @@ def __create_forest(self) -> None: """ self.forest_tl = self.__add_region("Kelp Forest top left area", AquariaLocations.locations_forest_tl) - self.forest_tl_fp = self.__add_region("Kelp Forest top left area fish pass", - AquariaLocations.locations_forest_tl_fp) + self.forest_tl_verse_egg_room = self.__add_region("Kelp Forest top left area fish pass", + AquariaLocations.locations_forest_tl_verse_egg_room) self.forest_tr = self.__add_region("Kelp Forest top right area", AquariaLocations.locations_forest_tr) self.forest_tr_fp = self.__add_region("Kelp Forest top right area fish pass", @@ -344,21 +375,21 @@ def __create_forest(self) -> None: AquariaLocations.locations_forest_bl_sc) self.forest_br = self.__add_region("Kelp Forest bottom right area", AquariaLocations.locations_forest_br) - self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave", - AquariaLocations.locations_forest_sprite_cave) - self.forest_sprite_cave_tube = self.__add_region("Kelp Forest spirit cave after the plant tube", - AquariaLocations.locations_forest_sprite_cave_tube) + self.sprite_cave = self.__add_region("Sprite cave", + AquariaLocations.locations_sprite_cave) + self.sprite_cave_tube = self.__add_region("Sprite cave after the plant tube", + AquariaLocations.locations_sprite_cave_tube) self.forest_boss = self.__add_region("Kelp Forest Drunian God room", AquariaLocations.locations_forest_boss) self.forest_boss_entrance = self.__add_region("Kelp Forest Drunian God room entrance", AquariaLocations.locations_forest_boss_entrance) - self.mermog_cave = self.__add_region("Kelp Forest Mermog cave", + self.mermog_cave = self.__add_region("Mermog cave", AquariaLocations.locations_mermog_cave) - self.mermog_boss = self.__add_region("Kelp Forest Mermog cave boss", + self.mermog_boss = self.__add_region("Mermog cave boss", AquariaLocations.locations_mermog_boss) self.forest_fish_cave = self.__add_region("Kelp Forest fish cave", AquariaLocations.locations_forest_fish_cave) - self.simon = self.__add_region("Kelp Forest, Simon's room", AquariaLocations.locations_simon) + self.simon = self.__add_region("Simon Says area", AquariaLocations.locations_simon) def __create_veil(self) -> None: """ @@ -373,18 +404,19 @@ def __create_veil(self) -> None: AquariaLocations.locations_turtle_cave_bubble) self.veil_tr_l = self.__add_region("The Veil top right area, left of temple", AquariaLocations.locations_veil_tr_l) + self.veil_tr_l_fp = self.__add_region("The Veil top right area, fish pass left of temple", None) self.veil_tr_r = self.__add_region("The Veil top right area, right of temple", AquariaLocations.locations_veil_tr_r) self.octo_cave_t = self.__add_region("Octopus Cave top entrance", AquariaLocations.locations_octo_cave_t) self.octo_cave_b = self.__add_region("Octopus Cave bottom entrance", AquariaLocations.locations_octo_cave_b) - self.veil_bl = self.__add_region("The Veil bottom left area", - AquariaLocations.locations_veil_bl) + self.veil_b = self.__add_region("The Veil bottom left area", + AquariaLocations.locations_veil_b) self.veil_b_sc = self.__add_region("The Veil bottom spirit crystal area", AquariaLocations.locations_veil_b_sc) - self.veil_bl_fp = self.__add_region("The Veil bottom left area, in the sunken ship", - AquariaLocations.locations_veil_bl_fp) + self.veil_b_fp = self.__add_region("The Veil bottom left area, in the sunken ship", + AquariaLocations.locations_veil_b_fp) self.veil_br = self.__add_region("The Veil bottom right area", AquariaLocations.locations_veil_br) @@ -394,6 +426,7 @@ def __create_sun_temple(self) -> None: """ self.sun_temple_l = self.__add_region("Sun Temple left area", AquariaLocations.locations_sun_temple_l) + self.sun_temple_l_entrance = self.__add_region("Sun Temple left area entrance", None) self.sun_temple_r = self.__add_region("Sun Temple right area", AquariaLocations.locations_sun_temple_r) self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area", @@ -412,24 +445,29 @@ def __create_abyss(self) -> None: self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r) self.abyss_r_transturtle = self.__add_region("Abyss right area, transturtle", AquariaLocations.locations_abyss_r_transturtle) - self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave) + self.abyss_r_whale = self.__add_region("Abyss right area, outside the whale", + AquariaLocations.locations_abyss_r_whale) + self.ice_cave = self.__add_region("Ice Cavern", AquariaLocations.locations_ice_cave) + self.frozen_feil = self.__add_region("Frozen Veil", None) self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave) self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss) self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave", AquariaLocations.locations_king_jellyfish_cave) self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale) - self.first_secret = self.__add_region("First secret area", None) + self.first_secret = self.__add_region("First Secret area", None) def __create_sunken_city(self) -> None: """ Create the `sunken_city_*` regions """ - self.sunken_city_l = self.__add_region("Sunken City left area", - AquariaLocations.locations_sunken_city_l) + self.sunken_city_l = self.__add_region("Sunken City left area", None) + self.sunken_city_l_crates = self.__add_region("Sunken City left area", + AquariaLocations.locations_sunken_city_l) self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom", AquariaLocations.locations_sunken_city_l_bedroom) - self.sunken_city_r = self.__add_region("Sunken City right area", - AquariaLocations.locations_sunken_city_r) + self.sunken_city_r = self.__add_region("Sunken City right area", None) + self.sunken_city_r_crates = self.__add_region("Sunken City right area crates", + AquariaLocations.locations_sunken_city_r) self.sunken_city_boss = self.__add_region("Sunken City boss area", AquariaLocations.locations_sunken_city_boss) @@ -454,249 +492,194 @@ def __create_body(self) -> None: AquariaLocations.locations_final_boss) self.final_boss_end = self.__add_region("The Body, final boss area", None) - def __connect_one_way_regions(self, source_name: str, destination_name: str, - source_region: Region, - destination_region: Region, rule=None) -> None: + def get_entrance_name(self, from_region: Region, to_region: Region): + """ + Return the name of an entrance between `from_region` and `to_region` + """ + return from_region.name + " to " + to_region.name + + def __connect_one_way_regions(self, source_region: Region, destination_region: Region, rule=None) -> None: """ Connect from the `source_region` to the `destination_region` """ - entrance = Entrance(source_region.player, source_name + " to " + destination_name, source_region) + entrance = Entrance(self.player, self.get_entrance_name(source_region, destination_region), source_region) source_region.exits.append(entrance) entrance.connect(destination_region) if rule is not None: set_rule(entrance, rule) - def __connect_regions(self, source_name: str, destination_name: str, - source_region: Region, + def __connect_regions(self, source_region: Region, destination_region: Region, rule=None) -> None: """ Connect the `source_region` and the `destination_region` (two-way) """ - self.__connect_one_way_regions(source_name, destination_name, source_region, destination_region, rule) - self.__connect_one_way_regions(destination_name, source_name, destination_region, source_region, rule) + self.__connect_one_way_regions(source_region, destination_region, rule) + self.__connect_one_way_regions(destination_region, source_region, rule) def __connect_home_water_regions(self) -> None: """ Connect entrances of the different regions around `home_water` """ - self.__connect_one_way_regions("Menu", "Verse Cave right area", - self.menu, self.verse_cave_r) - self.__connect_regions("Verse Cave left area", "Verse Cave right area", - self.verse_cave_l, self.verse_cave_r) - self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water) - self.__connect_regions("Home Water", "Haija's home", self.home_water, self.naija_home) - self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave) - self.__connect_regions("Home Water", "Home Water, nautilus nest", - self.home_water, self.home_water_nautilus, - lambda state: _has_energy_attack_item(state, self.player) and - _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Home Water transturtle room", - self.home_water, self.home_water_transturtle) - self.__connect_regions("Home Water", "Energy Temple first area", - self.home_water, self.energy_temple_1, + self.__connect_one_way_regions(self.menu, self.verse_cave_r) + self.__connect_regions(self.verse_cave_l, self.verse_cave_r) + self.__connect_regions(self.verse_cave_l, self.home_water) + self.__connect_regions(self.home_water, self.naija_home) + self.__connect_regions(self.home_water, self.song_cave) + self.__connect_regions(self.home_water, self.home_water_behind_rocks, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Energy Temple_altar", - self.home_water, self.energy_temple_altar, + self.__connect_regions(self.home_water_behind_rocks, self.home_water_nautilus, + lambda state: _has_energy_attack_item(state, self.player)) + self.__connect_regions(self.home_water, self.home_water_transturtle) + self.__connect_regions(self.home_water_behind_rocks, self.energy_temple_1) + self.__connect_regions(self.home_water_behind_rocks, self.energy_temple_altar, lambda state: _has_energy_attack_item(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Energy Temple first area", "Energy Temple second area", - self.energy_temple_1, self.energy_temple_2, + self.__connect_regions(self.energy_temple_1, self.energy_temple_2, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy Temple first area", "Energy Temple idol room", - self.energy_temple_1, self.energy_temple_idol, + self.__connect_regions(self.energy_temple_1, self.energy_temple_idol, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Energy Temple idol room", "Energy Temple boss area", - self.energy_temple_idol, self.energy_temple_boss, + self.__connect_regions(self.energy_temple_idol, self.energy_temple_boss, lambda state: _has_energy_attack_item(state, self.player) and _has_fish_form(state, self.player)) - self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area", - self.energy_temple_1, self.energy_temple_boss, - lambda state: _has_beast_form(state, self.player) and + self.__connect_one_way_regions(self.energy_temple_1, self.energy_temple_4, + lambda state: _has_beast_form(state, self.player)) + self.__connect_one_way_regions(self.energy_temple_4, self.energy_temple_1) + self.__connect_regions(self.energy_temple_4, self.energy_temple_boss, + lambda state: _has_energy_attack_item(state, self.player)) + self.__connect_regions(self.energy_temple_2, self.energy_temple_3) + self.__connect_one_way_regions(self.energy_temple_3, self.energy_temple_boss, + lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player)) - self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area", - self.energy_temple_boss, self.energy_temple_1, - lambda state: _has_energy_attack_item(state, self.player)) - self.__connect_regions("Energy Temple second area", "Energy Temple third area", - self.energy_temple_2, self.energy_temple_3, - lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room", - self.energy_temple_boss, self.energy_temple_blaster_room, - lambda state: _has_nature_form(state, self.player) and - _has_bind_song(state, self.player) and - _has_energy_attack_item(state, self.player)) - self.__connect_regions("Energy Temple first area", "Energy Temple blaster room", - self.energy_temple_1, self.energy_temple_blaster_room, - lambda state: _has_nature_form(state, self.player) and - _has_bind_song(state, self.player) and - _has_energy_attack_item(state, self.player) and - _has_beast_form(state, self.player)) - self.__connect_regions("Home Water", "Open Water top left area", - self.home_water, self.openwater_tl) + self.__connect_one_way_regions(self.energy_temple_4, self.energy_temple_blaster_room, + lambda state: _has_nature_form(state, self.player) and + _has_bind_song(state, self.player) and + _has_energy_attack_item(state, self.player)) + self.__connect_regions(self.home_water, self.openwater_tl) def __connect_open_water_regions(self) -> None: """ Connect entrances of the different regions around open water """ - self.__connect_regions("Open Water top left area", "Open Water top right area", - self.openwater_tl, self.openwater_tr) - self.__connect_regions("Open Water top left area", "Open Water bottom left area", - self.openwater_tl, self.openwater_bl) - self.__connect_regions("Open Water top left area", "forest bottom right area", - self.openwater_tl, self.forest_br) - self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room", - self.openwater_tr, self.openwater_tr_turtle, - lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) - self.__connect_regions("Open Water top right area", "Open Water bottom right area", - self.openwater_tr, self.openwater_br) - self.__connect_regions("Open Water top right area", "Mithalas City", - self.openwater_tr, self.mithalas_city) - self.__connect_regions("Open Water top right area", "Veil bottom left area", - self.openwater_tr, self.veil_bl) - self.__connect_one_way_regions("Open Water top right area", "Veil bottom right", - self.openwater_tr, self.veil_br, + self.__connect_regions(self.openwater_tl, self.openwater_tr) + self.__connect_regions(self.openwater_tl, self.openwater_bl) + self.__connect_regions(self.openwater_tl, self.forest_br) + self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_turtle, lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) - self.__connect_one_way_regions("Veil bottom right", "Open Water top right area", - self.veil_br, self.openwater_tr) - self.__connect_regions("Open Water bottom left area", "Open Water bottom right area", - self.openwater_bl, self.openwater_br) - self.__connect_regions("Open Water bottom left area", "Skeleton path", - self.openwater_bl, self.skeleton_path) - self.__connect_regions("Abyss left area", "Open Water bottom left area", - self.abyss_l, self.openwater_bl) - self.__connect_regions("Skeleton path", "skeleton_path_sc", - self.skeleton_path, self.skeleton_path_sc, + self.__connect_one_way_regions(self.openwater_tr_turtle, self.openwater_tr) + self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_urns, + lambda state: _has_bind_song(state, self.player) or + _has_damaging_item(state, self.player)) + self.__connect_regions(self.openwater_tr, self.openwater_br) + self.__connect_regions(self.openwater_tr, self.mithalas_city) + self.__connect_regions(self.openwater_tr, self.veil_b) + self.__connect_one_way_regions(self.openwater_tr, self.veil_br, + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) + self.__connect_one_way_regions(self.veil_br, self.openwater_tr) + self.__connect_regions(self.openwater_bl, self.openwater_br) + self.__connect_regions(self.openwater_bl, self.skeleton_path) + self.__connect_regions(self.abyss_l, self.openwater_bl) + self.__connect_regions(self.skeleton_path, self.skeleton_path_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Open Water bottom right area", - self.abyss_r, self.openwater_br) - self.__connect_one_way_regions("Open Water bottom right area", "Arnassi", - self.openwater_br, self.arnassi, + self.__connect_regions(self.abyss_r, self.openwater_br) + self.__connect_one_way_regions(self.openwater_br, self.arnassi, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Arnassi", "Open Water bottom right area", - self.arnassi, self.openwater_br) - self.__connect_regions("Arnassi", "Arnassi path", - self.arnassi, self.arnassi_path) - self.__connect_regions("Arnassi ruins, transturtle area", "Arnassi path", - self.arnassi_cave_transturtle, self.arnassi_path, + self.__connect_one_way_regions(self.arnassi, self.openwater_br) + self.__connect_regions(self.arnassi, self.arnassi_cave) + self.__connect_regions(self.arnassi_cave_transturtle, self.arnassi_cave, lambda state: _has_fish_form(state, self.player)) - self.__connect_one_way_regions("Arnassi path", "Arnassi crab boss area", - self.arnassi_path, self.arnassi_crab_boss, + self.__connect_one_way_regions(self.arnassi_cave, self.arnassi_crab_boss, lambda state: _has_beast_form_or_arnassi_armor(state, self.player) and (_has_energy_attack_item(state, self.player) or _has_nature_form(state, self.player))) - self.__connect_one_way_regions("Arnassi crab boss area", "Arnassi path", - self.arnassi_crab_boss, self.arnassi_path) + self.__connect_one_way_regions(self.arnassi_crab_boss, self.arnassi_cave) def __connect_mithalas_regions(self) -> None: """ Connect entrances of the different regions around Mithalas """ - self.__connect_one_way_regions("Mithalas City", "Mithalas City top path", - self.mithalas_city, self.mithalas_city_top_path, + self.__connect_one_way_regions(self.mithalas_city, self.mithalas_city_urns, + lambda state: _has_damaging_item(state, self.player)) + self.__connect_one_way_regions(self.mithalas_city, self.mithalas_city_top_path, lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) - self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City", - self.mithalas_city_top_path, self.mithalas_city) - self.__connect_regions("Mithalas City", "Mithalas City home with fishpass", - self.mithalas_city, self.mithalas_city_fishpass, + self.__connect_one_way_regions(self.mithalas_city_top_path, self.mithalas_city) + self.__connect_regions(self.mithalas_city, self.mithalas_city_fishpass, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Mithalas City", "Mithalas castle", - self.mithalas_city, self.cathedral_l) - self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube", - self.mithalas_city_top_path, - self.cathedral_l_tube, + self.__connect_regions(self.mithalas_city, self.mithalas_castle) + self.__connect_one_way_regions(self.mithalas_city_top_path, + self.mithalas_castle_tube, lambda state: _has_nature_form(state, self.player) and _has_energy_attack_item(state, self.player)) - self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path", - self.cathedral_l_tube, + self.__connect_one_way_regions(self.mithalas_castle_tube, self.mithalas_city_top_path, lambda state: _has_nature_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle flower tube area", "Mithalas castle, spirit crystals", - self.cathedral_l_tube, self.cathedral_l_sc, + self.__connect_one_way_regions(self.mithalas_castle_tube, self.mithalas_castle_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle_flower tube area", "Mithalas castle", - self.cathedral_l_tube, self.cathedral_l, + self.__connect_one_way_regions(self.mithalas_castle_tube, self.mithalas_castle, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Mithalas castle", "Mithalas castle, spirit crystals", - self.cathedral_l, self.cathedral_l_sc, + self.__connect_one_way_regions(self.mithalas_castle, self.mithalas_castle_urns, + lambda state: _has_damaging_item(state, self.player)) + self.__connect_regions(self.mithalas_castle, self.mithalas_castle_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle", "Cathedral boss right area", - self.cathedral_l, self.cathedral_boss_r, + self.__connect_one_way_regions(self.mithalas_castle, self.cathedral_boss_r, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Cathedral boss left area", "Mithalas castle", - self.cathedral_boss_l, self.cathedral_l, + self.__connect_one_way_regions(self.cathedral_boss_l, self.mithalas_castle, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground", - self.cathedral_l, self.cathedral_underground, + self.__connect_regions(self.mithalas_castle, self.cathedral_underground, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle", "Mithalas Cathedral", - self.cathedral_l, self.cathedral_r, - lambda state: _has_bind_song(state, self.player) and - _has_energy_attack_item(state, self.player)) - self.__connect_one_way_regions("Mithalas Cathedral", "Mithalas Cathedral underground", - self.cathedral_r, self.cathedral_underground) - self.__connect_one_way_regions("Mithalas Cathedral underground", "Mithalas Cathedral", - self.cathedral_underground, self.cathedral_r, + self.__connect_one_way_regions(self.mithalas_castle, self.cathedral_top_start, + lambda state: _has_bind_song(state, self.player)) + self.__connect_one_way_regions(self.cathedral_top_start, self.cathedral_top_start_urns, + lambda state: _has_damaging_item(state, self.player)) + self.__connect_regions(self.cathedral_top_start, self.cathedral_top_end, + lambda state: _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions(self.cathedral_underground, self.cathedral_top_end, lambda state: _has_beast_form(state, self.player) and - _has_energy_attack_item(state, self.player)) - self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss right area", - self.cathedral_underground, self.cathedral_boss_r) - self.__connect_one_way_regions("Cathedral boss right area", "Mithalas Cathedral underground", - self.cathedral_boss_r, self.cathedral_underground, + _has_damaging_item(state, self.player)) + self.__connect_one_way_regions(self.cathedral_top_end, self.cathedral_underground, + lambda state: _has_energy_attack_item(state, self.player) + ) + self.__connect_one_way_regions(self.cathedral_underground, self.cathedral_boss_r) + self.__connect_one_way_regions(self.cathedral_boss_r, self.cathedral_underground, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Cathedral boss right area", "Cathedral boss left area", - self.cathedral_boss_r, self.cathedral_boss_l, + self.__connect_one_way_regions(self.cathedral_boss_r, self.cathedral_boss_l, lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player)) - self.__connect_one_way_regions("Cathedral boss left area", "Cathedral boss right area", - self.cathedral_boss_l, self.cathedral_boss_r) + self.__connect_one_way_regions(self.cathedral_boss_l, self.cathedral_boss_r) def __connect_forest_regions(self) -> None: """ Connect entrances of the different regions around the Kelp Forest """ - self.__connect_regions("Forest bottom right", "Veil bottom left area", - self.forest_br, self.veil_bl) - self.__connect_regions("Forest bottom right", "Forest bottom left area", - self.forest_br, self.forest_bl) - self.__connect_one_way_regions("Forest bottom left area", "Forest bottom left area, spirit crystals", - self.forest_bl, self.forest_bl_sc, + self.__connect_regions(self.forest_br, self.veil_b) + self.__connect_regions(self.forest_br, self.forest_bl) + self.__connect_one_way_regions(self.forest_bl, self.forest_bl_sc, lambda state: _has_energy_attack_item(state, self.player) or _has_fish_form(state, self.player)) - self.__connect_one_way_regions("Forest bottom left area, spirit crystals", "Forest bottom left area", - self.forest_bl_sc, self.forest_bl) - self.__connect_regions("Forest bottom right", "Forest top right area", - self.forest_br, self.forest_tr) - self.__connect_regions("Forest bottom left area", "Forest fish cave", - self.forest_bl, self.forest_fish_cave) - self.__connect_regions("Forest bottom left area", "Forest top left area", - self.forest_bl, self.forest_tl) - self.__connect_regions("Forest bottom left area", "Forest boss entrance", - self.forest_bl, self.forest_boss_entrance, + self.__connect_one_way_regions(self.forest_bl_sc, self.forest_bl) + self.__connect_regions(self.forest_br, self.forest_tr) + self.__connect_regions(self.forest_bl, self.forest_fish_cave) + self.__connect_regions(self.forest_bl, self.forest_tl) + self.__connect_regions(self.forest_bl, self.forest_boss_entrance, lambda state: _has_nature_form(state, self.player)) - self.__connect_regions("Forest top left area", "Forest top left area, fish pass", - self.forest_tl, self.forest_tl_fp, - lambda state: _has_nature_form(state, self.player) and - _has_bind_song(state, self.player) and - _has_energy_attack_item(state, self.player) and - _has_fish_form(state, self.player)) - self.__connect_regions("Forest top left area", "Forest top right area", - self.forest_tl, self.forest_tr) - self.__connect_regions("Forest top left area", "Forest boss entrance", - self.forest_tl, self.forest_boss_entrance) - self.__connect_regions("Forest boss area", "Forest boss entrance", - self.forest_boss, self.forest_boss_entrance, - lambda state: _has_energy_attack_item(state, self.player)) - self.__connect_regions("Forest top right area", "Forest top right area fish pass", - self.forest_tr, self.forest_tr_fp, + self.__connect_one_way_regions(self.forest_tl, self.forest_tl_verse_egg_room, + lambda state: _has_nature_form(state, self.player) and + _has_bind_song(state, self.player) and + _has_energy_attack_item(state, self.player) and + _has_fish_form(state, self.player)) + self.__connect_one_way_regions(self.forest_tl_verse_egg_room, self.forest_tl, + lambda state: _has_fish_form(state, self.player)) + self.__connect_regions(self.forest_tl, self.forest_tr) + self.__connect_regions(self.forest_tl, self.forest_boss_entrance) + self.__connect_one_way_regions(self.forest_boss_entrance, self.forest_boss, + lambda state: _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions(self.forest_boss, self.forest_boss_entrance) + self.__connect_regions(self.forest_tr, self.forest_tr_fp, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Forest top right area", "Forest sprite cave", - self.forest_tr, self.forest_sprite_cave) - self.__connect_regions("Forest sprite cave", "Forest sprite cave flower tube", - self.forest_sprite_cave, self.forest_sprite_cave_tube, + self.__connect_regions(self.forest_tr, self.sprite_cave) + self.__connect_regions(self.sprite_cave, self.sprite_cave_tube, lambda state: _has_nature_form(state, self.player)) - self.__connect_regions("Forest top right area", "Mermog cave", - self.forest_tr_fp, self.mermog_cave) - self.__connect_regions("Fermog cave", "Fermog boss", - self.mermog_cave, self.mermog_boss, + self.__connect_regions(self.forest_tr_fp, self.mermog_cave) + self.__connect_regions(self.mermog_cave, self.mermog_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_attack_item(state, self.player)) @@ -704,113 +687,94 @@ def __connect_veil_regions(self) -> None: """ Connect entrances of the different regions around The Veil """ - self.__connect_regions("Veil bottom left area", "Veil bottom left area, fish pass", - self.veil_bl, self.veil_bl_fp, + self.__connect_regions(self.veil_b, self.veil_b_fp, lambda state: _has_fish_form(state, self.player) and - _has_bind_song(state, self.player) and - _has_damaging_item(state, self.player)) - self.__connect_regions("Veil bottom left area", "Veil bottom area spirit crystals path", - self.veil_bl, self.veil_b_sc, + _has_bind_song(state, self.player)) + self.__connect_regions(self.veil_b, self.veil_b_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Veil bottom area spirit crystals path", "Veil bottom right", - self.veil_b_sc, self.veil_br, + self.__connect_regions(self.veil_b_sc, self.veil_br, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Veil bottom right", "Veil top left area", - self.veil_br, self.veil_tl) - self.__connect_regions("Veil top left area", "Veil_top left area, fish pass", - self.veil_tl, self.veil_tl_fp, + self.__connect_regions(self.veil_br, self.veil_tl) + self.__connect_regions(self.veil_tl, self.veil_tl_fp, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Veil top left area", "Veil right of sun temple", - self.veil_tl, self.veil_tr_r) - self.__connect_regions("Veil top left area", "Turtle cave", - self.veil_tl, self.turtle_cave) - self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff", - self.turtle_cave, self.turtle_cave_bubble) - self.__connect_regions("Veil right of sun temple", "Sun Temple right area", - self.veil_tr_r, self.sun_temple_r) - self.__connect_one_way_regions("Sun Temple right area", "Sun Temple left area", - self.sun_temple_r, self.sun_temple_l, + self.__connect_regions(self.veil_tl, self.veil_tr_r) + self.__connect_regions(self.veil_tl, self.turtle_cave) + self.__connect_regions(self.turtle_cave, self.turtle_cave_bubble) + self.__connect_regions(self.veil_tr_r, self.sun_temple_r) + + self.__connect_one_way_regions(self.sun_temple_r, self.sun_temple_l_entrance, lambda state: _has_bind_song(state, self.player) or _has_light(state, self.player)) - self.__connect_one_way_regions("Sun Temple left area", "Sun Temple right area", - self.sun_temple_l, self.sun_temple_r, + self.__connect_one_way_regions(self.sun_temple_l_entrance, self.sun_temple_r, lambda state: _has_light(state, self.player)) - self.__connect_regions("Sun Temple left area", "Veil left of sun temple", - self.sun_temple_l, self.veil_tr_l) - self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", - self.sun_temple_l, self.sun_temple_boss_path) - self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", - self.sun_temple_boss_path, self.sun_temple_boss, + self.__connect_regions(self.sun_temple_l_entrance, self.veil_tr_l) + self.__connect_regions(self.sun_temple_l, self.sun_temple_l_entrance) + self.__connect_one_way_regions(self.sun_temple_l, self.sun_temple_boss_path) + self.__connect_one_way_regions(self.sun_temple_boss_path, self.sun_temple_l) + self.__connect_regions(self.sun_temple_boss_path, self.sun_temple_boss, lambda state: _has_energy_attack_item(state, self.player)) - self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple", - self.sun_temple_boss, self.veil_tr_l) - self.__connect_regions("Veil left of sun temple", "Octo cave top path", - self.veil_tr_l, self.octo_cave_t, - lambda state: _has_fish_form(state, self.player) and - _has_sun_form(state, self.player) and - _has_beast_form(state, self.player) and - _has_energy_attack_item(state, self.player)) - self.__connect_regions("Veil left of sun temple", "Octo cave bottom path", - self.veil_tr_l, self.octo_cave_b, + self.__connect_one_way_regions(self.sun_temple_boss, self.veil_tr_l) + self.__connect_regions(self.veil_tr_l, self.veil_tr_l_fp, lambda state: _has_fish_form(state, self.player)) + self.__connect_one_way_regions(self.veil_tr_l_fp, self.octo_cave_t, + lambda state: _has_sun_form(state, self.player) and + _has_beast_form(state, self.player) and + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions(self.octo_cave_t, self.veil_tr_l_fp) + self.__connect_regions(self.veil_tr_l_fp, self.octo_cave_b) def __connect_abyss_regions(self) -> None: """ Connect entrances of the different regions around The Abyss """ - self.__connect_regions("Abyss left area", "Abyss bottom of left area", - self.abyss_l, self.abyss_lb, + self.__connect_regions(self.abyss_l, self.abyss_lb, lambda state: _has_nature_form(state, self.player)) - self.__connect_regions("Abyss left bottom area", "Sunken City right area", - self.abyss_lb, self.sunken_city_r, + self.__connect_regions(self.abyss_lb, self.sunken_city_r, lambda state: _has_li(state, self.player)) - self.__connect_one_way_regions("Abyss left bottom area", "Body center area", - self.abyss_lb, self.body_c, + self.__connect_one_way_regions(self.abyss_lb, self.body_c, lambda state: _has_tongue_cleared(state, self.player)) - self.__connect_one_way_regions("Body center area", "Abyss left bottom area", - self.body_c, self.abyss_lb) - self.__connect_regions("Abyss left area", "King jellyfish cave", - self.abyss_l, self.king_jellyfish_cave, - lambda state: (_has_energy_form(state, self.player) and - _has_beast_form(state, self.player)) or - _has_dual_form(state, self.player)) - self.__connect_regions("Abyss left area", "Abyss right area", - self.abyss_l, self.abyss_r) - self.__connect_regions("Abyss right area", "Abyss right area, transturtle", - self.abyss_r, self.abyss_r_transturtle) - self.__connect_regions("Abyss right area", "Inside the whale", - self.abyss_r, self.whale, + self.__connect_one_way_regions(self.body_c, self.abyss_lb) + self.__connect_one_way_regions(self.abyss_l, self.king_jellyfish_cave, + lambda state: _has_dual_form(state, self.player) or + (_has_energy_form(state, self.player) and + _has_beast_form(state, self.player))) + self.__connect_one_way_regions(self.king_jellyfish_cave, self.abyss_l) + self.__connect_regions(self.abyss_l, self.abyss_r) + self.__connect_regions(self.abyss_r, self.abyss_r_whale, lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player)) - self.__connect_regions("Abyss right area", "First secret area", - self.abyss_r, self.first_secret, + self.__connect_regions(self.abyss_r_whale, self.whale) + self.__connect_regions(self.abyss_r, self.abyss_r_transturtle) + self.__connect_regions(self.abyss_r, self.first_secret, lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player)) - self.__connect_regions("Abyss right area", "Ice Cave", - self.abyss_r, self.ice_cave, + self.__connect_regions(self.abyss_r, self.ice_cave, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Ice cave", "Bubble Cave", - self.ice_cave, self.bubble_cave, - lambda state: _has_beast_form(state, self.player) or - _has_hot_soup(state, self.player)) - self.__connect_regions("Bubble Cave boss area", "Bubble Cave", - self.bubble_cave, self.bubble_cave_boss, - lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) - ) + self.__connect_regions(self.ice_cave, self.frozen_feil) + self.__connect_one_way_regions(self.frozen_feil, self.bubble_cave, + lambda state: _has_beast_form(state, self.player) or + _has_hot_soup(state, self.player)) + self.__connect_one_way_regions(self.bubble_cave, self.frozen_feil) + self.__connect_one_way_regions(self.bubble_cave, self.bubble_cave_boss, + lambda state: _has_nature_form(state, self.player) and + _has_bind_song(state, self.player) + ) + self.__connect_one_way_regions(self.bubble_cave_boss, self.bubble_cave) def __connect_sunken_city_regions(self) -> None: """ Connect entrances of the different regions around The Sunken City """ - self.__connect_regions("Sunken City right area", "Sunken City left area", - self.sunken_city_r, self.sunken_city_l) - self.__connect_regions("Sunken City left area", "Sunken City bedroom", - self.sunken_city_l, self.sunken_city_l_bedroom, + self.__connect_regions(self.sunken_city_r, self.sunken_city_l) + self.__connect_one_way_regions(self.sunken_city_r, self.sunken_city_r_crates, + lambda state: _has_energy_attack_item(state, self.player)) + self.__connect_regions(self.sunken_city_l, self.sunken_city_l_bedroom, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Sunken City left area", "Sunken City boss area", - self.sunken_city_l, self.sunken_city_boss, + self.__connect_one_way_regions(self.sunken_city_l, self.sunken_city_l_crates, + lambda state: _has_energy_attack_item(state, self.player)) + self.__connect_regions(self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and _has_sun_form(state, self.player) and _has_energy_attack_item(state, self.player) and @@ -820,62 +784,55 @@ def __connect_body_regions(self) -> None: """ Connect entrances of the different regions around The Body """ - self.__connect_regions("Body center area", "Body left area", - self.body_c, self.body_l, - lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Body center area", "Body right area top path", - self.body_c, self.body_rt) - self.__connect_regions("Body center area", "Body right area bottom path", - self.body_c, self.body_rb, - lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Body center area", "Body bottom area", - self.body_c, self.body_b, + self.__connect_one_way_regions(self.body_c, self.body_l, + lambda state: _has_energy_form(state, self.player)) + self.__connect_one_way_regions(self.body_l, self.body_c) + self.__connect_regions(self.body_c, self.body_rt) + self.__connect_one_way_regions(self.body_c, self.body_rb, + lambda state: _has_energy_form(state, self.player)) + self.__connect_one_way_regions(self.body_rb, self.body_c) + self.__connect_regions(self.body_c, self.body_b, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Body bottom area", "Final Boss area", - self.body_b, self.final_boss_loby, + self.__connect_regions(self.body_b, self.final_boss_loby, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Before Final Boss", "Final Boss tube", - self.final_boss_loby, self.final_boss_tube, + self.__connect_regions(self.final_boss_loby, self.final_boss_tube, lambda state: _has_nature_form(state, self.player)) - self.__connect_one_way_regions("Before Final Boss", "Final Boss", - self.final_boss_loby, self.final_boss, + self.__connect_one_way_regions(self.final_boss_loby, self.final_boss, lambda state: _has_energy_form(state, self.player) and _has_dual_form(state, self.player) and _has_sun_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_one_way_regions("final boss third form area", "final boss end", - self.final_boss, self.final_boss_end) + self.__connect_one_way_regions(self.final_boss, self.final_boss_end) - def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, - region_target: Region) -> None: + def __connect_transturtle(self, item_target: str, region_source: Region, region_target: Region) -> None: """Connect a single transturtle to another one""" - if item_source != item_target: - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, + if region_source != region_target: + self.__connect_one_way_regions(region_source, region_target, lambda state: state.has(item_target, self.player)) - def _connect_transturtle_to_other(self, item: str, region: Region) -> None: + def _connect_transturtle_to_other(self, region: Region) -> None: """Connect a single transturtle to all others""" - self.__connect_transturtle(item, "Transturtle Veil top left", region, self.veil_tl) - self.__connect_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) - self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) - self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) - self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r_transturtle) - self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon) - self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_cave_transturtle) + self.__connect_transturtle(ItemNames.TRANSTURTLE_VEIL_TOP_LEFT, region, self.veil_tl) + self.__connect_transturtle(ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT, region, self.veil_tr_l) + self.__connect_transturtle(ItemNames.TRANSTURTLE_OPEN_WATERS, region, self.openwater_tr_turtle) + self.__connect_transturtle(ItemNames.TRANSTURTLE_KELP_FOREST, region, self.forest_bl) + self.__connect_transturtle(ItemNames.TRANSTURTLE_HOME_WATERS, region, self.home_water_transturtle) + self.__connect_transturtle(ItemNames.TRANSTURTLE_ABYSS, region, self.abyss_r_transturtle) + self.__connect_transturtle(ItemNames.TRANSTURTLE_BODY, region, self.final_boss_tube) + self.__connect_transturtle(ItemNames.TRANSTURTLE_SIMON_SAYS, region, self.simon) + self.__connect_transturtle(ItemNames.TRANSTURTLE_ARNASSI_RUINS, region, self.arnassi_cave_transturtle) def __connect_transturtles(self) -> None: """Connect every transturtle with others""" - self._connect_transturtle_to_other("Transturtle Veil top left", self.veil_tl) - self._connect_transturtle_to_other("Transturtle Veil top right", self.veil_tr_l) - self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle) - self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl) - self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle) - self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r_transturtle) - self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube) - self._connect_transturtle_to_other("Transturtle Simon Says", self.simon) - self._connect_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_cave_transturtle) + self._connect_transturtle_to_other(self.veil_tl) + self._connect_transturtle_to_other(self.veil_tr_l) + self._connect_transturtle_to_other(self.openwater_tr_turtle) + self._connect_transturtle_to_other(self.forest_bl) + self._connect_transturtle_to_other(self.home_water_transturtle) + self._connect_transturtle_to_other(self.abyss_r_transturtle) + self._connect_transturtle_to_other(self.final_boss_tube) + self._connect_transturtle_to_other(self.simon) + self._connect_transturtle_to_other(self.arnassi_cave_transturtle) def connect_regions(self) -> None: """ @@ -910,20 +867,20 @@ def __add_event_big_bosses(self) -> None: Add every bit bosses (other than the creator) events to the `world` """ self.__add_event_location(self.energy_temple_boss, - "Beating Fallen God", - "Fallen God beated") + AquariaLocationNames.BEATING_FALLEN_GOD, + ItemNames.FALLEN_GOD_BEATED) self.__add_event_location(self.cathedral_boss_l, - "Beating Mithalan God", - "Mithalan God beated") + AquariaLocationNames.BEATING_MITHALAN_GOD, + ItemNames.MITHALAN_GOD_BEATED) self.__add_event_location(self.forest_boss, - "Beating Drunian God", - "Drunian God beated") + AquariaLocationNames.BEATING_DRUNIAN_GOD, + ItemNames.DRUNIAN_GOD_BEATED) self.__add_event_location(self.sun_temple_boss, - "Beating Sun God", - "Sun God beated") + AquariaLocationNames.BEATING_LUMEREAN_GOD, + ItemNames.LUMEREAN_GOD_BEATED) self.__add_event_location(self.sunken_city_boss, - "Beating the Golem", - "The Golem beated") + AquariaLocationNames.BEATING_THE_GOLEM, + ItemNames.THE_GOLEM_BEATED) def __add_event_mini_bosses(self) -> None: """ @@ -931,43 +888,44 @@ def __add_event_mini_bosses(self) -> None: events to the `world` """ self.__add_event_location(self.home_water_nautilus, - "Beating Nautilus Prime", - "Nautilus Prime beated") + AquariaLocationNames.BEATING_NAUTILUS_PRIME, + ItemNames.NAUTILUS_PRIME_BEATED) self.__add_event_location(self.energy_temple_blaster_room, - "Beating Blaster Peg Prime", - "Blaster Peg Prime beated") + AquariaLocationNames.BEATING_BLASTER_PEG_PRIME, + ItemNames.BLASTER_PEG_PRIME_BEATED) self.__add_event_location(self.mermog_boss, - "Beating Mergog", - "Mergog beated") - self.__add_event_location(self.cathedral_l_tube, - "Beating Mithalan priests", - "Mithalan priests beated") + AquariaLocationNames.BEATING_MERGOG, + ItemNames.MERGOG_BEATED) + self.__add_event_location(self.mithalas_castle_tube, + AquariaLocationNames.BEATING_MITHALAN_PRIESTS, + ItemNames.MITHALAN_PRIESTS_BEATED) self.__add_event_location(self.octo_cave_t, - "Beating Octopus Prime", - "Octopus Prime beated") + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + ItemNames.OCTOPUS_PRIME_BEATED) self.__add_event_location(self.arnassi_crab_boss, - "Beating Crabbius Maximus", - "Crabbius Maximus beated") + AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS, + ItemNames.CRABBIUS_MAXIMUS_BEATED) self.__add_event_location(self.bubble_cave_boss, - "Beating Mantis Shrimp Prime", - "Mantis Shrimp Prime beated") + AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME, + ItemNames.MANTIS_SHRIMP_PRIME_BEATED) self.__add_event_location(self.king_jellyfish_cave, - "Beating King Jellyfish God Prime", - "King Jellyfish God Prime beated") + AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME, + ItemNames.KING_JELLYFISH_GOD_PRIME_BEATED) def __add_event_secrets(self) -> None: """ Add secrets events to the `world` """ - self.__add_event_location(self.first_secret, # Doit ajouter une région pour le "first secret" - "First secret", - "First secret obtained") + self.__add_event_location(self.first_secret, + # Doit ajouter une région pour le AquariaLocationNames.FIRST_SECRET + AquariaLocationNames.FIRST_SECRET, + ItemNames.FIRST_SECRET_OBTAINED) self.__add_event_location(self.mithalas_city, - "Second secret", - "Second secret obtained") + AquariaLocationNames.SECOND_SECRET, + ItemNames.SECOND_SECRET_OBTAINED) self.__add_event_location(self.sun_temple_l, - "Third secret", - "Third secret obtained") + AquariaLocationNames.THIRD_SECRET, + ItemNames.THIRD_SECRET_OBTAINED) def add_event_locations(self) -> None: """ @@ -977,287 +935,236 @@ def add_event_locations(self) -> None: self.__add_event_big_bosses() self.__add_event_secrets() self.__add_event_location(self.sunken_city_boss, - "Sunken City cleared", - "Body tongue cleared") + AquariaLocationNames.SUNKEN_CITY_CLEARED, + ItemNames.BODY_TONGUE_CLEARED) self.__add_event_location(self.sun_temple_r, - "Sun Crystal", - "Has sun crystal") - self.__add_event_location(self.final_boss_end, "Objective complete", - "Victory") - - def __adjusting_urns_rules(self) -> None: - """Since Urns need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule( - self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, second urn in one of the homes", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, first urn in the city reserve", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, second urn in the city reserve", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City Castle, first urn of the single lamp path", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City Castle, second urn of the single lamp path", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bottom room", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City Castle, first urn on the entrance path", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City Castle, second urn on the entrance path", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), - lambda state: _has_damaging_item(state, self.player)) - - def __adjusting_crates_rules(self) -> None: - """Since Crate need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Sunken City right area, crate close to the save crystal", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken City right area, crate in the left bottom room", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken City left area, crate in the little pipe room", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken City left area, crate close to the save crystal", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken City left area, crate before the bedroom", self.player), - lambda state: _has_damaging_item(state, self.player)) + AquariaLocationNames.SUN_CRYSTAL, + ItemNames.HAS_SUN_CRYSTAL) + self.__add_event_location(self.final_boss_end, AquariaLocationNames.OBJECTIVE_COMPLETE, + ItemNames.VICTORY) def __adjusting_soup_rules(self) -> None: """ Modify rules for location that need soup """ - add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME, self.player), lambda state: _has_hot_soup(state, self.player)) - add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player), - lambda state: _has_beast_and_soup_form(state, self.player)) + add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, self.player), + lambda state: _has_beast_and_soup_form(state, self.player) or + state.has(ItemNames.LUMEREAN_GOD_BEATED, self.player), combine="or") + add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, self.player), + lambda state: _has_beast_and_soup_form(state, self.player) or + state.has(ItemNames.LUMEREAN_GOD_BEATED, self.player), combine="or") + add_rule( + self.multiworld.get_location(AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + self.player), + lambda state: _has_beast_and_soup_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: """ Modify rules implying bind song needed for bulb under rocks """ - add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", + add_rule(self.multiworld.get_location( + AquariaLocationNames.HOME_WATERS_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH_FROM_THE_VERSE_CAVE, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.VERSE_CAVE_LEFT_AREA_BULB_UNDER_THE_ROCK_AT_THE_END_OF_THE_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.NAIJA_S_HOME_BULB_UNDER_THE_ROCK_AT_THE_RIGHT_OF_THE_MAIN_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location(AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR, self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path", + add_rule(self.multiworld.get_location(AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK, self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location( + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule( + self.multiworld.get_location(AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location(AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH, self.player), lambda state: _has_bind_song(state, self.player)) + add_rule(self.multiworld.get_location( + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH, + self.player), lambda state: _has_bind_song(state, self.player)) def __adjusting_light_in_dark_place_rules(self) -> None: - add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL, self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player), + add_rule( + self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER, self.player), + lambda state: _has_light(state, self.player)) + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.sun_temple_l_entrance, self.sun_temple_l), + self.player), lambda state: _has_light(state, self.player) or + _has_sun_crystal(state, self.player)) + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.sun_temple_boss_path, self.sun_temple_l), + self.player), lambda state: _has_light(state, self.player) or + _has_sun_crystal(state, self.player)) + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.abyss_r_transturtle, self.abyss_r), + self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open Water bottom right area to Abyss right area", self.player), + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.body_c, self.abyss_lb), self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.openwater_br, self.abyss_r), self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), - lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Abyss right area, transturtle to Abyss right area", self.player), + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.openwater_bl, self.abyss_l), self.player), lambda state: _has_light(state, self.player)) def __adjusting_manual_rules(self) -> None: - add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS, self.player), lambda state: _has_beast_form(state, self.player)) - add_rule( - self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player), + add_rule(self.multiworld.get_location( + AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS, self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player), - lambda state: _has_spirit_form(state, self.player)) add_rule( - self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player), + self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY, self.player), + lambda state: _has_spirit_form(state, self.player)) + add_rule( + self.multiworld.get_location( + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK, self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG, self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS, + self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Song Cave, Anemone Seed", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.SONG_CAVE_ANEMONE_SEED, self.player), lambda state: _has_nature_form(state, self.player)) - add_rule(self.multiworld.get_location("Song Cave, Verse Egg", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.SONG_CAVE_VERSE_EGG, self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED, self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE, self.player), lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) - add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock", - self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player), - lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player), - lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player), - lambda state: _has_energy_attack_item(state, self.player)) - add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), - lambda state: _has_spirit_form(state, self.player) and - _has_sun_form(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player), + add_rule(self.multiworld.get_location( + AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK, + self.player), lambda state: _has_bind_song(state, self.player)) + add_rule( + self.multiworld.get_location(AquariaLocationNames.NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR, self.player), + lambda state: _has_energy_attack_item(state, self.player)) + add_rule(self.multiworld.get_location(AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR, self.player), lambda state: _has_fish_form(state, self.player) or _has_beast_and_soup_form(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), - lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player), + add_rule( + self.multiworld.get_location(AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS, self.player), + lambda state: _has_damaging_item(state, self.player)) + add_rule(self.multiworld.get_location(AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE, + self.player), lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location( - "The Veil top right area, bulb in the middle of the wall jump cliff", self.player + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF, self.player ), lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) - add_rule(self.multiworld.get_location("Kelp Forest top left area, Jelly Egg", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG, self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, self.player), lambda state: state.has("Sun God beated", self.player)) - add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), + add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, self.player), lambda state: state.has("Sun God beated", self.player)) - add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), - lambda state: _has_tongue_cleared(state, self.player)) + add_rule( + self.multiworld.get_location(AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, self.player), + lambda state: _has_tongue_cleared(state, self.player)) add_rule(self.multiworld.get_location( - "Open Water top right area, bulb in the small path before Mithalas", - self.player), lambda state: _has_bind_song(state, self.player) + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS, + self.player), lambda state: _has_bind_song(state, self.player) ) def __no_progression_hard_or_hidden_location(self) -> None: - self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Mithalas boss area, beating Mithalan God", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Sun Temple boss area, beating Sun God", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Sunken City, bulb on top of the boss area", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Home Water, Nautilus Egg", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Mithalas City Castle, beating the Priests", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Mermog cave, Piranha Egg", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Octopus Cave, Dumbo Egg", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Final Boss area, bulb in the boss third form room", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Sun Worm path, first cliff bulb", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Sun Worm path, second cliff bulb", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Bubble Cave, Verse Egg", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Sun Temple, Sun Key", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("The Body bottom area, Mutant Costume", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", - self.player).item_rule = \ - lambda item: not item.advancement - self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", - self.player).item_rule = \ - lambda item: not item.advancement + self.multiworld.get_location(AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location( + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location( + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_SUN_KEY, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART, + self.player).item_rule = _item_not_advancement + self.multiworld.get_location(AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR, + self.player).item_rule = _item_not_advancement def adjusting_rules(self, options: AquariaOptions) -> None: """ Modify rules for single location or optional rules """ - self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player) - self.__adjusting_urns_rules() - self.__adjusting_crates_rules() - self.__adjusting_soup_rules() self.__adjusting_manual_rules() + self.__adjusting_soup_rules() if options.light_needed_to_get_to_dark_places: self.__adjusting_light_in_dark_place_rules() if options.bind_song_needed_to_get_under_rock_bulb: self.__adjusting_under_rock_location() if options.mini_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), - lambda state: _has_mini_bosses(state, self.player)) + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_loby, self.final_boss), + self.player), lambda state: _has_mini_bosses(state, self.player)) if options.big_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), - lambda state: _has_big_bosses(state, self.player)) - if options.objective.value == 1: - add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), - lambda state: _has_secrets(state, self.player)) - if options.unconfine_home_water.value in [0, 1]: - add_rule(self.multiworld.get_entrance("Home Water to Home Water transturtle room", self.player), - lambda state: _has_bind_song(state, self.player)) - if options.unconfine_home_water.value in [0, 2]: - add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player), - lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player)) - if options.early_energy_form: - self.multiworld.early_items[self.player]["Energy form"] = 1 - + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_loby, self.final_boss), + self.player), lambda state: _has_big_bosses(state, self.player)) + if options.objective.value == options.objective.option_obtain_secrets_and_kill_the_creator: + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_loby, self.final_boss), + self.player), lambda state: _has_secrets(state, self.player)) + if (options.unconfine_home_water.value == UnconfineHomeWater.option_via_energy_door or + options.unconfine_home_water.value == UnconfineHomeWater.option_off): + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.home_water, self.home_water_transturtle), + self.player), lambda state: _has_bind_song(state, self.player)) + if (options.unconfine_home_water.value == UnconfineHomeWater.option_via_transturtle or + options.unconfine_home_water.value == UnconfineHomeWater.option_off): + add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.home_water, self.openwater_tl), + self.player), + lambda state: _has_bind_song(state, self.player) and + _has_energy_attack_item(state, self.player)) if options.no_progression_hard_or_hidden_locations: self.__no_progression_hard_or_hidden_location() @@ -1292,9 +1199,9 @@ def __add_open_water_regions_to_world(self) -> None: self.multiworld.regions.append(self.skeleton_path) self.multiworld.regions.append(self.skeleton_path_sc) self.multiworld.regions.append(self.arnassi) - self.multiworld.regions.append(self.arnassi_path) - self.multiworld.regions.append(self.arnassi_crab_boss) + self.multiworld.regions.append(self.arnassi_cave) self.multiworld.regions.append(self.arnassi_cave_transturtle) + self.multiworld.regions.append(self.arnassi_crab_boss) self.multiworld.regions.append(self.simon) def __add_mithalas_regions_to_world(self) -> None: @@ -1304,10 +1211,12 @@ def __add_mithalas_regions_to_world(self) -> None: self.multiworld.regions.append(self.mithalas_city) self.multiworld.regions.append(self.mithalas_city_top_path) self.multiworld.regions.append(self.mithalas_city_fishpass) - self.multiworld.regions.append(self.cathedral_l) - self.multiworld.regions.append(self.cathedral_l_tube) - self.multiworld.regions.append(self.cathedral_l_sc) - self.multiworld.regions.append(self.cathedral_r) + self.multiworld.regions.append(self.mithalas_castle) + self.multiworld.regions.append(self.mithalas_castle_tube) + self.multiworld.regions.append(self.mithalas_castle_sc) + self.multiworld.regions.append(self.cathedral_top_start) + self.multiworld.regions.append(self.cathedral_top_start_urns) + self.multiworld.regions.append(self.cathedral_top_end) self.multiworld.regions.append(self.cathedral_underground) self.multiworld.regions.append(self.cathedral_boss_l) self.multiworld.regions.append(self.cathedral_boss_r) @@ -1317,7 +1226,7 @@ def __add_forest_regions_to_world(self) -> None: Add every region around the kelp forest to the `world` """ self.multiworld.regions.append(self.forest_tl) - self.multiworld.regions.append(self.forest_tl_fp) + self.multiworld.regions.append(self.forest_tl_verse_egg_room) self.multiworld.regions.append(self.forest_tr) self.multiworld.regions.append(self.forest_tr_fp) self.multiworld.regions.append(self.forest_bl) @@ -1325,8 +1234,8 @@ def __add_forest_regions_to_world(self) -> None: self.multiworld.regions.append(self.forest_br) self.multiworld.regions.append(self.forest_boss) self.multiworld.regions.append(self.forest_boss_entrance) - self.multiworld.regions.append(self.forest_sprite_cave) - self.multiworld.regions.append(self.forest_sprite_cave_tube) + self.multiworld.regions.append(self.sprite_cave) + self.multiworld.regions.append(self.sprite_cave_tube) self.multiworld.regions.append(self.mermog_cave) self.multiworld.regions.append(self.mermog_boss) self.multiworld.regions.append(self.forest_fish_cave) @@ -1338,16 +1247,18 @@ def __add_veil_regions_to_world(self) -> None: self.multiworld.regions.append(self.veil_tl) self.multiworld.regions.append(self.veil_tl_fp) self.multiworld.regions.append(self.veil_tr_l) + self.multiworld.regions.append(self.veil_tr_l_fp) self.multiworld.regions.append(self.veil_tr_r) - self.multiworld.regions.append(self.veil_bl) + self.multiworld.regions.append(self.veil_b) self.multiworld.regions.append(self.veil_b_sc) - self.multiworld.regions.append(self.veil_bl_fp) + self.multiworld.regions.append(self.veil_b_fp) self.multiworld.regions.append(self.veil_br) self.multiworld.regions.append(self.octo_cave_t) self.multiworld.regions.append(self.octo_cave_b) self.multiworld.regions.append(self.turtle_cave) self.multiworld.regions.append(self.turtle_cave_bubble) self.multiworld.regions.append(self.sun_temple_l) + self.multiworld.regions.append(self.sun_temple_l_entrance) self.multiworld.regions.append(self.sun_temple_r) self.multiworld.regions.append(self.sun_temple_boss_path) self.multiworld.regions.append(self.sun_temple_boss) @@ -1359,6 +1270,7 @@ def __add_abyss_regions_to_world(self) -> None: self.multiworld.regions.append(self.abyss_l) self.multiworld.regions.append(self.abyss_lb) self.multiworld.regions.append(self.abyss_r) + self.multiworld.regions.append(self.abyss_r_whale) self.multiworld.regions.append(self.abyss_r_transturtle) self.multiworld.regions.append(self.ice_cave) self.multiworld.regions.append(self.bubble_cave) diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index f620bf6d7306..1f7b956bb34b 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -7,9 +7,10 @@ from typing import List, Dict, ClassVar, Any from worlds.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 .Items import item_table, AquariaItem, ItemType, ItemGroup, ItemNames +from .Locations import location_table, AquariaLocationNames +from .Options import (AquariaOptions, IngredientRandomizer, TurtleRandomizer, EarlyBindSong, EarlyEnergyForm, + UnconfineHomeWater, Objective) from .Regions import AquariaRegions @@ -65,15 +66,15 @@ class AquariaWorld(World): web: WebWorld = AquariaWeb() "The web page generation informations" - item_name_to_id: ClassVar[Dict[str, int]] =\ + 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"} + "Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, + ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, + ItemNames.BABY_BLASTER}, + "Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO} } """Grouping item make it easier to find them""" @@ -148,23 +149,32 @@ def get_filler_item_name(self): 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) + if self.options.turtle_randomizer.value != TurtleRandomizer.option_none: + if self.options.turtle_randomizer.value == TurtleRandomizer.option_all_except_final: + self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.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", + self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_LEFT, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE, precollected) + self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE, precollected) + self.__pre_fill_item(ItemNames.TRANSTURTLE_OPEN_WATERS, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE, precollected) - self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle", + self.__pre_fill_item(ItemNames.TRANSTURTLE_KELP_FOREST, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE, + precollected) + self.__pre_fill_item(ItemNames.TRANSTURTLE_HOME_WATERS, AquariaLocationNames.HOME_WATERS_TRANSTURTLE, + precollected) + self.__pre_fill_item(ItemNames.TRANSTURTLE_ABYSS, AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE, + precollected) + self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_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, - ItemClassification.progression) - self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) + self.__pre_fill_item(ItemNames.TRANSTURTLE_SIMON_SAYS, AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE, + precollected, ItemClassification.progression) + self.__pre_fill_item(ItemNames.TRANSTURTLE_ARNASSI_RUINS, AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE, + precollected) for name, data in item_table.items(): if name not in self.exclude: for i in range(data.count): @@ -175,10 +185,17 @@ def set_rules(self) -> None: """ Launched when the Multiworld generator is ready to generate rules """ - + if self.options.early_energy_form == EarlyEnergyForm.option_early: + self.multiworld.early_items[self.player][ItemNames.ENERGY_FORM] = 1 + elif self.options.early_energy_form == EarlyEnergyForm.option_early_and_local: + self.multiworld.local_early_items[self.player][ItemNames.ENERGY_FORM] = 1 + if self.options.early_bind_song == EarlyBindSong.option_early: + self.multiworld.early_items[self.player][ItemNames.BIND_SONG] = 1 + elif self.options.early_bind_song == EarlyBindSong.option_early_and_local: + self.multiworld.local_early_items[self.player][ItemNames.BIND_SONG] = 1 self.regions.adjusting_rules(self.options) self.multiworld.completion_condition[self.player] = lambda \ - state: state.has("Victory", self.player) + state: state.has(ItemNames.VICTORY, self.player) def generate_basic(self) -> None: """ @@ -186,13 +203,13 @@ def generate_basic(self) -> None: 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: + if self.options.ingredient_randomizer.value > IngredientRandomizer.option_off: + if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients: 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: + if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients: simple_ingredients_substitution.extend([24, 25, 26]) dishes_substitution = [i for i in range(27, 76)] if self.options.dish_randomizer: @@ -205,14 +222,19 @@ def fill_slot_data(self) -> Dict[str, Any]: return {"ingredientReplacement": self.ingredients_substitution, "aquarian_translate": bool(self.options.aquarian_translation.value), "blind_goal": bool(self.options.blind_goal.value), - "secret_needed": self.options.objective.value > 0, + "secret_needed": + self.options.objective.value == Objective.option_obtain_secrets_and_kill_the_creator, "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], + "unconfine_home_water_energy_door": + self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_energy_door + or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both, + "unconfine_home_water_transturtle": + self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_transturtle + or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both, "bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb), "no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations), "light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places), - "turtle_randomizer": self.options.turtle_randomizer.value, + "turtle_randomizer": self.options.turtle_randomizer.value } diff --git a/worlds/aquaria/docs/en_Aquaria.md b/worlds/aquaria/docs/en_Aquaria.md index c3e5f54dd66a..836a942be741 100644 --- a/worlds/aquaria/docs/en_Aquaria.md +++ b/worlds/aquaria/docs/en_Aquaria.md @@ -24,7 +24,7 @@ The locations in the randomizer are: * Beating Mithalan God boss * Fish Cave puzzle * Beating Drunian God boss - * Beating Sun God boss + * Beating Lumerean God boss * Breaking Li cage in the body Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates, diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index 8c4f64c3452c..05f46fc76259 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -6,211 +6,212 @@ from test.bases import WorldTestBase +from ..Locations import AquariaLocationNames # 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 crystal", - "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 crystal", - "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 lowest 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", - "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 Castle 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 City 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, fourth urn in the top right room", - "Mithalas Cathedral, Mithalan Dress", - "Mithalas Cathedral, urn below 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 crystal", - "Cathedral Underground, bulb in the bottom right path", - "Mithalas 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 area, 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 Li's 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", - "The Veil top right area, bulb at the top of the waterfall", - "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 crystal)", - "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 right 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 bulb in the top exit room", - "Ice Cave, second bulb in the top exit room", - "Ice Cave, third bulb 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 crystal", - "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 crystal", - "Sunken City left area, crate before the bedroom", - "Sunken City left area, Girl Costume", - "Sunken City, bulb on top of the boss area", - "The Body center area, breaking Li's cage", - "The Body center 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 below 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 bulb in the turtle room", - "Final Boss area, third bulb in the turtle room", - "Final Boss area, Transturtle", - "Final Boss area, bulb in the boss third form room", - "Simon Says area, 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", + AquariaLocationNames.SUN_CRYSTAL, + AquariaLocationNames.HOME_WATERS_TRANSTURTLE, + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH, + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH, + AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_TO_THE_RIGHT_OF_THE_SAVE_CRYSTAL, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_PATH_FROM_THE_LEFT_ENTRANCE, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_CLEARING_CLOSE_TO_THE_BOTTOM_EXIT, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_TO_THE_TOP_EXIT, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_FIRST_URN_IN_THE_MITHALAS_EXIT, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_SECOND_URN_IN_THE_MITHALAS_EXIT, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_THIRD_URN_IN_THE_MITHALAS_EXIT, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_TURTLE_ROOM, + AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE, + AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_BEHIND_THE_CHOMPER_FISH, + AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS, + AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_CLOSE_TO_THE_RIGHT_EXIT, + AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_BEHIND_THE_CHOMPER_FISH, + AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_KING_SKULL, + AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART, + AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_LEFT_PART, + AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_CENTER_PART, + AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE, + AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR, + AquariaLocationNames.ARNASSI_RUINS_ARNASSI_STATUE, + AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE, + AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR, + AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_THE_LEFT_CITY_PART, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_THE_LEFT_CITY_PART, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_RIGHT_PART, + AquariaLocationNames.MITHALAS_CITY_BULB_AT_THE_TOP_OF_THE_CITY, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_A_BROKEN_HOME, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_A_BROKEN_HOME, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_BOTTOM_LEFT_PART, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_ONE_OF_THE_HOMES, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_ONE_OF_THE_HOMES, + AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_ONE_OF_THE_HOMES, + AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_ONE_OF_THE_HOMES, + AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_THE_CITY_RESERVE, + AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_THE_CITY_RESERVE, + AquariaLocationNames.MITHALAS_CITY_THIRD_URN_IN_THE_CITY_RESERVE, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH, + AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT, + AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE, + AquariaLocationNames.MITHALAS_CITY_DOLL, + AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BLUE_BANNER, + AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BEDROOM, + AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_OF_THE_SINGLE_LAMP_PATH, + AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_OF_THE_SINGLE_LAMP_PATH, + AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BOTTOM_ROOM, + AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_ON_THE_ENTRANCE_PATH, + AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_ON_THE_ENTRANCE_PATH, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD, + AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_TOP_RIGHT_ROOM, + AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_TOP_RIGHT_ROOM, + AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_TOP_RIGHT_ROOM, + AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_BOTTOM_RIGHT_PATH, + AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_BOTTOM_RIGHT_PATH, + AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BEHIND_THE_FLESH_VEIN, + AquariaLocationNames.MITHALAS_CATHEDRAL_URN_IN_THE_TOP_LEFT_EYES_BOSS_ROOM, + AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN, + AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN, + AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN, + AquariaLocationNames.MITHALAS_CATHEDRAL_FOURTH_URN_IN_THE_TOP_RIGHT_ROOM, + AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS, + AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BELOW_THE_LEFT_ENTRANCE, + AquariaLocationNames.MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_CENTER_PART, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_FIRST_BULB_IN_THE_TOP_LEFT_PART, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_SECOND_BULB_IN_THE_TOP_LEFT_PART, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_THIRD_BULB_IN_THE_TOP_LEFT_PART, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_BOTTOM_RIGHT_PATH, + AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_PATH_DOWN_FROM_THE_TOP_LEFT_CLEARING, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_LEFT_OF_THE_CENTER_CLEARING, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_BIG_ROOM, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_ROOM, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_CENTER_CLEARING, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE, + AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER, + AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD, + AquariaLocationNames.KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE, + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_INSIDE_THE_FISH_PASS, + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM, + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG, + AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE, + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS, + AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG, + AquariaLocationNames.TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF, + AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_STONE_HEAD, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL, + AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART, + AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM, + AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR, + AquariaLocationNames.SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE, + AquariaLocationNames.SUN_TEMPLE_BULB_ON_THE_RIGHT_PART, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART, + AquariaLocationNames.SUN_TEMPLE_SUN_KEY, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, + AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART, + AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED, + AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM, + AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT, + AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM, + AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY, + AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME, + AquariaLocationNames.THE_WHALE_VERSE_EGG, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, + AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE, + AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_FIRST_BULB_IN_THE_TURTLE_ROOM, + AquariaLocationNames.FINAL_BOSS_AREA_SECOND_BULB_IN_THE_TURTLE_ROOM, + AquariaLocationNames.FINAL_BOSS_AREA_THIRD_BULB_IN_THE_TURTLE_ROOM, + AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS, + AquariaLocationNames.BEATING_FALLEN_GOD, + AquariaLocationNames.BEATING_MITHALAN_GOD, + AquariaLocationNames.BEATING_DRUNIAN_GOD, + AquariaLocationNames.BEATING_LUMEREAN_GOD, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.BEATING_NAUTILUS_PRIME, + AquariaLocationNames.BEATING_BLASTER_PEG_PRIME, + AquariaLocationNames.BEATING_MERGOG, + AquariaLocationNames.BEATING_MITHALAN_PRIESTS, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS, + AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME, + AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME, + AquariaLocationNames.FIRST_SECRET, + AquariaLocationNames.SECOND_SECRET, + AquariaLocationNames.THIRD_SECRET, + AquariaLocationNames.SUNKEN_CITY_CLEARED, + AquariaLocationNames.OBJECTIVE_COMPLETE, ] class AquariaTestBase(WorldTestBase): diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index c09586269d38..684c33115ffc 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -5,6 +5,8 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames class BeastFormAccessTest(AquariaTestBase): @@ -13,16 +15,16 @@ class BeastFormAccessTest(AquariaTestBase): def test_beast_form_location(self) -> None: """Test locations that require beast form""" locations = [ - "Mermog cave, Piranha Egg", - "Kelp Forest top left area, Jelly Egg", - "Mithalas Cathedral, Mithalan Dress", - "The Veil top right area, bulb at the top of the waterfall", - "Sunken City, bulb on top of the boss area", - "Octopus Cave, Dumbo Egg", - "Beating the Golem", - "Beating Mergog", - "Beating Octopus Prime", - "Sunken City cleared", + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG, + AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.BEATING_MERGOG, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.SUNKEN_CITY_CLEARED, ] - items = [["Beast form"]] + items = [[ItemNames.BEAST_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py b/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py index fa4c6923400a..4c93c309a119 100644 --- a/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py +++ b/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py @@ -5,6 +5,8 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames class BeastForArnassiArmormAccessTest(AquariaTestBase): @@ -13,27 +15,27 @@ class BeastForArnassiArmormAccessTest(AquariaTestBase): def test_beast_form_arnassi_armor_location(self) -> None: """Test locations that require beast form or arnassi armor""" locations = [ - "Mithalas City Castle, beating the Priests", - "Arnassi Ruins, Crab Armor", - "Arnassi Ruins, Song Plant Spore", - "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 Castle flower tube entrance", - "Mermog cave, Piranha Egg", - "Mithalas Cathedral, Mithalan Dress", - "Kelp Forest top left area, Jelly Egg", - "The Veil top right area, bulb in the middle of the wall jump cliff", - "The Veil top right area, bulb at the top of the waterfall", - "Sunken City, bulb on top of the boss area", - "Octopus Cave, Dumbo Egg", - "Beating the Golem", - "Beating Mergog", - "Beating Crabbius Maximus", - "Beating Octopus Prime", - "Beating Mithalan priests", - "Sunken City cleared" + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR, + AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE, + AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH, + AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH, + AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH, + AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT, + AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.BEATING_MERGOG, + AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.BEATING_MITHALAN_PRIESTS, + AquariaLocationNames.SUNKEN_CITY_CLEARED ] - items = [["Beast form", "Arnassi Armor"]] + items = [[ItemNames.BEAST_FORM, ItemNames.ARNASSI_ARMOR]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py index 05f96edb9192..689f487c644e 100644 --- a/worlds/aquaria/test/test_bind_song_access.py +++ b/worlds/aquaria/test/test_bind_song_access.py @@ -6,31 +6,36 @@ """ from . import AquariaTestBase, after_home_water_locations +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import UnconfineHomeWater, EarlyBindSong 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, + "unconfine_home_water": UnconfineHomeWater.option_off, + "early_bind_song": EarlyBindSong.option_off } 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 below 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", + AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM, + AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG, + AquariaLocationNames.SONG_CAVE_VERSE_EGG, + AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE, + AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK, + AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL, + AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK, + AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR, + AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH, + AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH, + AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, *after_home_water_locations ] - items = [["Bind song"]] + items = [[ItemNames.BIND_SONG]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py index e391eef101bf..74dfa2ed7094 100644 --- a/worlds/aquaria/test/test_bind_song_option_access.py +++ b/worlds/aquaria/test/test_bind_song_option_access.py @@ -7,6 +7,8 @@ from . import AquariaTestBase from .test_bind_song_access import after_home_water_locations +from ..Items import ItemNames +from ..Locations import AquariaLocationNames class BindSongOptionAccessTest(AquariaTestBase): @@ -18,25 +20,25 @@ class BindSongOptionAccessTest(AquariaTestBase): 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 below 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", + AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED, + AquariaLocationNames.VERSE_CAVE_LEFT_AREA_BULB_UNDER_THE_ROCK_AT_THE_END_OF_THE_PATH, + AquariaLocationNames.HOME_WATERS_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH_FROM_THE_VERSE_CAVE, + AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR, + AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES, + AquariaLocationNames.NAIJA_S_HOME_BULB_UNDER_THE_ROCK_AT_THE_RIGHT_OF_THE_MAIN_PATH, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME, + AquariaLocationNames.HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM, + AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG, + AquariaLocationNames.SONG_CAVE_VERSE_EGG, + AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE, + AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK, + AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL, + AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK, + AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR, + AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH, + AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH, + AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, *after_home_water_locations ] - items = [["Bind song"]] + items = [[ItemNames.BIND_SONG]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_confined_home_water.py b/worlds/aquaria/test/test_confined_home_water.py index 89c51ac5c775..d809a3d5cb68 100644 --- a/worlds/aquaria/test/test_confined_home_water.py +++ b/worlds/aquaria/test/test_confined_home_water.py @@ -5,16 +5,17 @@ """ from . import AquariaTestBase +from ..Options import UnconfineHomeWater, EarlyEnergyForm 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 + "unconfine_home_water": UnconfineHomeWater.option_off, + "early_energy_form": EarlyEnergyForm.option_off } 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") + self.assertFalse(self.can_reach_region("Open Waters top left area"), "Can reach Open Waters top left area") + self.assertFalse(self.can_reach_region("Home Waters, turtle room"), "Can reach Home Waters, turtle room") diff --git a/worlds/aquaria/test/test_dual_song_access.py b/worlds/aquaria/test/test_dual_song_access.py index bb9b2e739604..448d9df0ef3e 100644 --- a/worlds/aquaria/test/test_dual_song_access.py +++ b/worlds/aquaria/test/test_dual_song_access.py @@ -5,22 +5,25 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import TurtleRandomizer class LiAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without the dual song""" options = { - "turtle_randomizer": 1, + "turtle_randomizer": TurtleRandomizer.option_all, } 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" + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.OBJECTIVE_COMPLETE ] - items = [["Dual form"]] + items = [[ItemNames.DUAL_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index b443166823bc..7eeb7c2e73c4 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -6,28 +6,31 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import EarlyEnergyForm class EnergyFormAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without the energy form""" options = { - "early_energy_form": False, + "early_energy_form": EarlyEnergyForm.option_off } def test_energy_form_location(self) -> None: """Test locations that require Energy form""" locations = [ - "Energy Temple second area, bulb under the rock", - "Energy Temple third area, bulb in the bottom path", - "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 below 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 path to the bottom face room", - "The Body right area, bulb in the bottom face room", - "Final Boss area, bulb in the boss third form room", - "Objective complete", + AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK, + AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH, + AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.OBJECTIVE_COMPLETE, ] - items = [["Energy form"]] + items = [[ItemNames.ENERGY_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_energy_form_or_dual_form_access.py b/worlds/aquaria/test/test_energy_form_or_dual_form_access.py index 8a765bc4e4e2..ba04405eea59 100644 --- a/worlds/aquaria/test/test_energy_form_or_dual_form_access.py +++ b/worlds/aquaria/test/test_energy_form_or_dual_form_access.py @@ -5,88 +5,74 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import EarlyEnergyForm, TurtleRandomizer class EnergyFormDualFormAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)""" options = { - "early_energy_form": False, + "early_energy_form": EarlyEnergyForm.option_off, + "turtle_randomizer": TurtleRandomizer.option_all } def test_energy_form_or_dual_form_location(self) -> None: """Test locations that require Energy form or dual form""" locations = [ - "Naija's Home, bulb after the energy door", - "Home Water, Nautilus Egg", - "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 blaster room, Blaster Egg", - "Energy Temple boss area, Fallen God Tooth", - "Mithalas City Castle, beating the Priests", - "Mithalas boss area, beating Mithalan God", - "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, fourth urn in the top right room", - "Mithalas Cathedral, Mithalan Dress", - "Mithalas Cathedral, urn below the left entrance", - "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", - "King Jellyfish Cave, bulb in the right path from King Jelly", - "King Jellyfish Cave, Jellyfish Costume", - "Sunken City right area, crate close to the save crystal", - "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 crystal", - "Sunken City left area, crate before the bedroom", - "Sunken City left area, Girl Costume", - "Sunken City, bulb on top of the boss area", - "The Body center area, breaking Li's cage", - "The Body center 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 below 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", - "Final Boss area, first bulb in the turtle room", - "Final Boss area, second bulb in the turtle room", - "Final Boss area, third bulb in the turtle room", - "Final Boss area, Transturtle", - "Beating Fallen God", - "Beating Blaster Peg Prime", - "Beating Mithalan God", - "Beating Drunian God", - "Beating Sun God", - "Beating the Golem", - "Beating Nautilus Prime", - "Beating Mergog", - "Beating Mithalan priests", - "Beating Octopus Prime", - "Beating King Jellyfish God Prime", - "Beating the Golem", - "Sunken City cleared", - "First secret", - "Objective complete" + AquariaLocationNames.NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR, + AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG, + AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK, + AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR, + AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH, + AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, + AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD, + AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY, + AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, + AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE, + AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.BEATING_FALLEN_GOD, + AquariaLocationNames.BEATING_BLASTER_PEG_PRIME, + AquariaLocationNames.BEATING_MITHALAN_GOD, + AquariaLocationNames.BEATING_DRUNIAN_GOD, + AquariaLocationNames.BEATING_LUMEREAN_GOD, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.BEATING_NAUTILUS_PRIME, + AquariaLocationNames.BEATING_MERGOG, + AquariaLocationNames.BEATING_MITHALAN_PRIESTS, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.SUNKEN_CITY_CLEARED, + AquariaLocationNames.FIRST_SECRET, + AquariaLocationNames.OBJECTIVE_COMPLETE ] - items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]] + items = [[ItemNames.ENERGY_FORM, ItemNames.DUAL_FORM, ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index 40b15a87cd35..3cbc750c7039 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -5,33 +5,36 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import TurtleRandomizer class FishFormAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without the fish form""" options = { - "turtle_randomizer": 1, + "turtle_randomizer": TurtleRandomizer.option_all, } def test_fish_form_location(self) -> None: """Test locations that require fish form""" locations = [ - "The Veil top left area, bulb inside the fish pass", - "Energy Temple first area, Energy Idol", - "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 lowest 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" + AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS, + AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL, + AquariaLocationNames.MITHALAS_CITY_DOLL, + AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS, + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG, + AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG, + AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.BEATING_MERGOG, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS ] - items = [["Fish form"]] + items = [[ItemNames.FISH_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index f615fb10c640..6c8d6e5eebba 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -5,41 +5,44 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import TurtleRandomizer class LiAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without Li""" options = { - "turtle_randomizer": 1, + "turtle_randomizer": TurtleRandomizer.option_all, } def test_li_song_location(self) -> None: """Test locations that require Li""" locations = [ - "Sunken City right area, crate close to the save crystal", - "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 crystal", - "Sunken City left area, crate before the bedroom", - "Sunken City left area, Girl Costume", - "Sunken City, bulb on top of the boss area", - "The Body center area, breaking Li's cage", - "The Body center 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 below 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" + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, + AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE, + AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.SUNKEN_CITY_CLEARED, + AquariaLocationNames.OBJECTIVE_COMPLETE ] - items = [["Li and Li song", "Body tongue cleared"]] + items = [[ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index 29d37d790b20..ca668505f7d2 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -5,12 +5,15 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import TurtleRandomizer class LightAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without light""" options = { - "turtle_randomizer": 1, + "turtle_randomizer": TurtleRandomizer.option_all, "light_needed_to_get_to_dark_places": True, } @@ -19,52 +22,52 @@ def test_light_location(self) -> None: 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", - "Ice Cave, bulb in the room to the right", - "Ice Cave, first bulb in the top exit room", - "Ice Cave, second bulb in the top exit room", - "Ice Cave, third bulb 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 crystal)", - "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 crystal", - "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 crystal", - "Sunken City left area, crate before the bedroom", - "Sunken City left area, Girl Costume", - "Sunken City, bulb on top of the boss area", - "Sunken City cleared", - "Beating the Golem", - "Beating Octopus Prime", - "Final Boss area, bulb in the boss third form room", - "Objective complete", + # AquariaLocationNames.THIRD_SECRET, + # AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART, + # AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART, + # AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM, + # AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR, + # AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB, + # AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB, + # AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER, + AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART, + AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED, + AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT, + AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL, + AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME, + AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY, + AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME, + AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME, + AquariaLocationNames.THE_WHALE_VERSE_EGG, + AquariaLocationNames.FIRST_SECRET, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.SUNKEN_CITY_CLEARED, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.OBJECTIVE_COMPLETE, ] - items = [["Sun form", "Baby Dumbo", "Has sun crystal"]] + items = [[ItemNames.SUN_FORM, ItemNames.BABY_DUMBO, ItemNames.HAS_SUN_CRYSTAL]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index 1d3b8f4150eb..61aebaef4816 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -5,53 +5,56 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames +from ..Options import TurtleRandomizer class NatureFormAccessTest(AquariaTestBase): """Unit test used to test accessibility of locations with and without the nature form""" options = { - "turtle_randomizer": 1, + "turtle_randomizer": TurtleRandomizer.option_all, } 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 City 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 crystal", - "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 crystal", - "Sunken City left area, crate before the bedroom", - "Sunken City left area, Girl Costume", - "Sunken City, bulb on top of the boss area", - "Beating the Golem", - "Sunken City cleared", - "The Body center area, breaking Li's cage", - "The Body center 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 below 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" + AquariaLocationNames.SONG_CAVE_ANEMONE_SEED, + AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, + AquariaLocationNames.BEATING_BLASTER_PEG_PRIME, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM, + AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG, + AquariaLocationNames.BEATING_MITHALAN_PRIESTS, + AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS, + AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.SUNKEN_CITY_CLEARED, + AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, + AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE, + AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.OBJECTIVE_COMPLETE ] - items = [["Nature form"]] + items = [[ItemNames.NATURE_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index 517af3028dd2..65139088f27c 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -6,6 +6,7 @@ from . import AquariaTestBase from BaseClasses import ItemClassification +from ..Locations import AquariaLocationNames class UNoProgressionHardHiddenTest(AquariaTestBase): @@ -15,31 +16,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy Temple boss area, Fallen God Tooth", - "Mithalas boss area, beating Mithalan God", - "Kelp Forest boss area, beating Drunian God", - "Sun Temple boss area, beating Sun God", - "Sunken City, bulb on top of the boss area", - "Home Water, Nautilus Egg", - "Energy Temple blaster room, Blaster Egg", - "Mithalas City 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 at the top of the waterfall", - "Bubble Cave, bulb in the left cave wall", - "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - "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", + AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH, + AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD, + AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD, + AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG, + AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY, + AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL, + AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY, + AquariaLocationNames.SUN_TEMPLE_SUN_KEY, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART, + AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR, ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index a1493c5d0f39..f6ac8e0e17e2 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -5,6 +5,7 @@ """ from . import AquariaTestBase +from ..Locations import AquariaLocationNames class UNoProgressionHardHiddenTest(AquariaTestBase): @@ -14,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy Temple boss area, Fallen God Tooth", - "Mithalas boss area, beating Mithalan God", - "Kelp Forest boss area, beating Drunian God", - "Sun Temple boss area, beating Sun God", - "Sunken City, bulb on top of the boss area", - "Home Water, Nautilus Egg", - "Energy Temple blaster room, Blaster Egg", - "Mithalas City 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 at the top of the waterfall", - "Bubble Cave, bulb in the left cave wall", - "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - "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", + AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH, + AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD, + AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD, + AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG, + AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG, + AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS, + AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY, + AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, + AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, + AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL, + AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY, + AquariaLocationNames.SUN_TEMPLE_SUN_KEY, + AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME, + AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART, + AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR, ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 7e31de9905e9..834661e0bd4d 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -5,6 +5,8 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames class SpiritFormAccessTest(AquariaTestBase): @@ -13,23 +15,23 @@ class SpiritFormAccessTest(AquariaTestBase): 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 bulb in the top exit room", - "Ice Cave, second bulb in the top exit room", - "Ice Cave, third bulb 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 crystal)", - "Bubble Cave, Verse Egg", - "Sunken City left area, Girl Costume", - "Beating Mantis Shrimp Prime", - "First secret", + AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH, + AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD, + AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_KING_SKULL, + AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM, + AquariaLocationNames.THE_WHALE_VERSE_EGG, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT, + AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM, + AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL, + AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL, + AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG, + AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME, + AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME, + AquariaLocationNames.FIRST_SECRET, ] - items = [["Spirit form"]] + items = [[ItemNames.SPIRIT_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py index 394d5e4b27ae..b37cceeed9c3 100644 --- a/worlds/aquaria/test/test_sun_form_access.py +++ b/worlds/aquaria/test/test_sun_form_access.py @@ -5,6 +5,8 @@ """ from . import AquariaTestBase +from ..Items import ItemNames +from ..Locations import AquariaLocationNames class SunFormAccessTest(AquariaTestBase): @@ -13,16 +15,16 @@ class SunFormAccessTest(AquariaTestBase): 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", - "Sunken City, bulb on top of the boss area", - "Beating the Golem", - "Sunken City cleared", - "Final Boss area, bulb in the boss third form room", - "Objective complete" + AquariaLocationNames.FIRST_SECRET, + AquariaLocationNames.THE_WHALE_VERSE_EGG, + AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM, + AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG, + AquariaLocationNames.BEATING_OCTOPUS_PRIME, + AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA, + AquariaLocationNames.BEATING_THE_GOLEM, + AquariaLocationNames.SUNKEN_CITY_CLEARED, + AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM, + AquariaLocationNames.OBJECTIVE_COMPLETE ] - items = [["Sun form"]] + items = [[ItemNames.SUN_FORM]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_both.py b/worlds/aquaria/test/test_unconfine_home_water_via_both.py index 5b8689bc53a2..038e27782a16 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_both.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_both.py @@ -6,16 +6,17 @@ """ from . import AquariaTestBase +from ..Options import UnconfineHomeWater, EarlyEnergyForm 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 + "unconfine_home_water": UnconfineHomeWater.option_via_both, + "early_energy_form": EarlyEnergyForm.option_off } 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") + self.assertTrue(self.can_reach_region("Open Waters top left area"), "Cannot reach Open Waters top left area") + self.assertTrue(self.can_reach_region("Home Waters, turtle room"), "Cannot reach Home Waters, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py index 37a5c98610b5..269a4b33837e 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py @@ -5,16 +5,17 @@ """ from . import AquariaTestBase +from ..Options import UnconfineHomeWater, EarlyEnergyForm 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 + "unconfine_home_water": UnconfineHomeWater.option_via_energy_door, + "early_energy_form": EarlyEnergyForm.option_off } 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") + self.assertTrue(self.can_reach_region("Open Waters top left area"), "Cannot reach Open Waters top left area") + self.assertFalse(self.can_reach_region("Home Waters, turtle room"), "Can reach Home Waters, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py index da4c83c2bc7f..b8efb82471c4 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py @@ -5,16 +5,17 @@ """ from . import AquariaTestBase +from ..Options import UnconfineHomeWater, EarlyEnergyForm 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 + "unconfine_home_water": UnconfineHomeWater.option_via_transturtle, + "early_energy_form": EarlyEnergyForm.option_off } 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") + self.assertTrue(self.can_reach_region("Home Waters, turtle room"), "Cannot reach Home Waters, turtle room") + self.assertFalse(self.can_reach_region("Open Waters top left area"), "Can reach Open Waters top left area") From 51c4fe8f67511850a7d26fe07a183242683034f1 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sun, 8 Dec 2024 21:00:30 -0500 Subject: [PATCH 101/112] Stardew Valley: Fix a bug where walnutsanity would get deactivated even tho ginger island got forced activated (and move some files) (#4311) --- worlds/stardew_valley/__init__.py | 40 +- worlds/stardew_valley/items.py | 32 +- worlds/stardew_valley/logic/walnut_logic.py | 22 +- worlds/stardew_valley/option_groups.py | 76 ---- worlds/stardew_valley/options/__init__.py | 6 + .../stardew_valley/options/forced_options.py | 48 +++ .../stardew_valley/options/option_groups.py | 68 ++++ .../stardew_valley/{ => options}/options.py | 23 +- worlds/stardew_valley/options/presets.py | 371 +++++++++++++++++ worlds/stardew_valley/presets.py | 376 ------------------ worlds/stardew_valley/rules.py | 10 +- .../strings/ap_names/ap_option_names.py | 35 +- .../strings/ap_names/mods/__init__.py | 0 worlds/stardew_valley/test/TestBooksanity.py | 1 - worlds/stardew_valley/test/TestOptions.py | 34 +- .../stardew_valley/test/TestOptionsPairs.py | 19 +- worlds/stardew_valley/test/TestRegions.py | 9 +- .../stardew_valley/test/TestWalnutsanity.py | 12 +- worlds/stardew_valley/test/__init__.py | 59 +-- worlds/stardew_valley/test/mods/TestMods.py | 5 +- .../test/options/TestForcedOptions.py | 84 ++++ .../test/{ => options}/TestPresets.py | 10 +- .../stardew_valley/test/options/__init__.py | 0 worlds/stardew_valley/test/options/utils.py | 68 ++++ .../test/stability/TestUniversalTracker.py | 4 +- 25 files changed, 752 insertions(+), 660 deletions(-) delete mode 100644 worlds/stardew_valley/option_groups.py create mode 100644 worlds/stardew_valley/options/__init__.py create mode 100644 worlds/stardew_valley/options/forced_options.py create mode 100644 worlds/stardew_valley/options/option_groups.py rename worlds/stardew_valley/{ => options}/options.py (97%) create mode 100644 worlds/stardew_valley/options/presets.py delete mode 100644 worlds/stardew_valley/presets.py create mode 100644 worlds/stardew_valley/strings/ap_names/mods/__init__.py create mode 100644 worlds/stardew_valley/test/options/TestForcedOptions.py rename worlds/stardew_valley/test/{ => options}/TestPresets.py (86%) create mode 100644 worlds/stardew_valley/test/options/__init__.py create mode 100644 worlds/stardew_valley/test/options/utils.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 34c617f5013a..6ba0e35e0a3a 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -3,7 +3,7 @@ from typing import Dict, Any, Iterable, Optional, Union, List, TextIO from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState -from Options import PerGameCommonOptions, Accessibility +from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from . import rules from .bundles.bundle_room import BundleRoom @@ -15,10 +15,11 @@ from .logic.bundle_logic import BundleLogic from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS -from .option_groups import sv_option_groups -from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, EnabledFillerBuffs, NumberOfMovementBuffs, \ - BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType, Walnutsanity -from .presets import sv_options_presets +from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, NumberOfMovementBuffs, \ + BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType, Walnutsanity +from .options.forced_options import force_change_options_if_incompatible +from .options.option_groups import sv_option_groups +from .options.presets import sv_options_presets from .regions import create_regions from .rules import set_rules from .stardew_rule import True_, StardewRule, HasProgressionPercent, true_ @@ -112,36 +113,9 @@ def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Optional[int]: return seed def generate_early(self): - self.force_change_options_if_incompatible() + force_change_options_if_incompatible(self.options, self.player, self.player_name) self.content = create_content(self.options) - def force_change_options_if_incompatible(self): - goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter - goal_is_perfection = self.options.goal == Goal.option_perfection - goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection - exclude_ginger_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - - if goal_is_island_related and exclude_ginger_island: - self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false - goal_name = self.options.goal.current_key - logger.warning( - f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({self.player_name})") - - if exclude_ginger_island and self.options.walnutsanity != Walnutsanity.preset_none: - self.options.walnutsanity.value = Walnutsanity.preset_none - logger.warning( - f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({self.player_name})'s world, so walnutsanity was force disabled") - - if goal_is_perfection and self.options.accessibility == Accessibility.option_minimal: - self.options.accessibility.value = Accessibility.option_full - logger.warning( - f"Goal 'Perfection' requires full accessibility. Accessibility setting forced to 'Full' for player {self.player} ({self.player_name})") - - elif self.options.goal == Goal.option_allsanity and self.options.accessibility == Accessibility.option_minimal: - self.options.accessibility.value = Accessibility.option_full - logger.warning( - f"Goal 'Allsanity' requires full accessibility. Accessibility setting forced to 'Full' for player {self.player} ({self.player_name})") - def create_regions(self): def create_region(name: str, exits: Iterable[str]) -> Region: region = Region(name, self.player, self.multiworld) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 3d852a37f402..6ac827f869cc 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -17,7 +17,7 @@ from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs -from .strings.ap_names.ap_option_names import OptionName +from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.buff_names import Buff from .strings.ap_names.community_upgrade_names import CommunityUpgrade @@ -538,16 +538,16 @@ def create_walnuts(item_factory: StardewItemFactory, options: StardewValleyOptio num_penta_walnuts = 1 # https://stardewvalleywiki.com/Golden_Walnut # Totals should be accurate, but distribution is slightly offset to make room for baseline walnuts - if OptionName.walnutsanity_puzzles in walnutsanity: # 61 + if WalnutsanityOptionName.puzzles in walnutsanity: # 61 num_single_walnuts += 6 # 6 num_triple_walnuts += 5 # 15 num_penta_walnuts += 8 # 40 - if OptionName.walnutsanity_bushes in walnutsanity: # 25 + if WalnutsanityOptionName.bushes in walnutsanity: # 25 num_single_walnuts += 16 # 16 num_triple_walnuts += 3 # 9 - if OptionName.walnutsanity_dig_spots in walnutsanity: # 18 + if WalnutsanityOptionName.dig_spots in walnutsanity: # 18 num_single_walnuts += 18 # 18 - if OptionName.walnutsanity_repeatables in walnutsanity: # 33 + if WalnutsanityOptionName.repeatables in walnutsanity: # 33 num_single_walnuts += 30 # 30 num_triple_walnuts += 1 # 3 @@ -833,27 +833,27 @@ def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> Li def get_allowed_player_buffs(buff_option: EnabledFillerBuffs) -> List[ItemData]: allowed_buffs = [] - if OptionName.buff_luck in buff_option: + if BuffOptionName.luck in buff_option: allowed_buffs.append(item_table[Buff.luck]) - if OptionName.buff_damage in buff_option: + if BuffOptionName.damage in buff_option: allowed_buffs.append(item_table[Buff.damage]) - if OptionName.buff_defense in buff_option: + if BuffOptionName.defense in buff_option: allowed_buffs.append(item_table[Buff.defense]) - if OptionName.buff_immunity in buff_option: + if BuffOptionName.immunity in buff_option: allowed_buffs.append(item_table[Buff.immunity]) - if OptionName.buff_health in buff_option: + if BuffOptionName.health in buff_option: allowed_buffs.append(item_table[Buff.health]) - if OptionName.buff_energy in buff_option: + if BuffOptionName.energy in buff_option: allowed_buffs.append(item_table[Buff.energy]) - if OptionName.buff_bite in buff_option: + if BuffOptionName.bite in buff_option: allowed_buffs.append(item_table[Buff.bite_rate]) - if OptionName.buff_fish_trap in buff_option: + if BuffOptionName.fish_trap in buff_option: allowed_buffs.append(item_table[Buff.fish_trap]) - if OptionName.buff_fishing_bar in buff_option: + if BuffOptionName.fishing_bar in buff_option: allowed_buffs.append(item_table[Buff.fishing_bar]) - if OptionName.buff_quality in buff_option: + if BuffOptionName.quality in buff_option: allowed_buffs.append(item_table[Buff.quality]) - if OptionName.buff_glow in buff_option: + if BuffOptionName.glow in buff_option: allowed_buffs.append(item_table[Buff.glow]) return allowed_buffs diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py index 14fe1c339090..4ab3b46f70d9 100644 --- a/worlds/stardew_valley/logic/walnut_logic.py +++ b/worlds/stardew_valley/logic/walnut_logic.py @@ -7,10 +7,10 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin -from ..strings.ap_names.event_names import Event from ..options import ExcludeGingerIsland, Walnutsanity from ..stardew_rule import StardewRule, False_, True_ -from ..strings.ap_names.ap_option_names import OptionName +from ..strings.ap_names.ap_option_names import WalnutsanityOptionName +from ..strings.ap_names.event_names import Event from ..strings.craftable_names import Furniture from ..strings.crop_names import Fruit from ..strings.metal_names import Mineral, Fossil @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin, - AbilityLogicMixin]]): +AbilityLogicMixin]]): def has_walnut(self, number: int) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: @@ -44,22 +44,22 @@ def has_walnut(self, number: int) -> StardewRule: total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts walnuts_to_receive = 0 walnuts_to_collect = number - if OptionName.walnutsanity_puzzles in self.options.walnutsanity: + if WalnutsanityOptionName.puzzles in self.options.walnutsanity: puzzle_walnut_rate = puzzle_walnuts / total_walnuts puzzle_walnuts_required = round(puzzle_walnut_rate * number) walnuts_to_receive += puzzle_walnuts_required walnuts_to_collect -= puzzle_walnuts_required - if OptionName.walnutsanity_bushes in self.options.walnutsanity: + if WalnutsanityOptionName.bushes in self.options.walnutsanity: bush_walnuts_rate = bush_walnuts / total_walnuts bush_walnuts_required = round(bush_walnuts_rate * number) walnuts_to_receive += bush_walnuts_required walnuts_to_collect -= bush_walnuts_required - if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: + if WalnutsanityOptionName.dig_spots in self.options.walnutsanity: dig_walnuts_rate = dig_walnuts / total_walnuts dig_walnuts_required = round(dig_walnuts_rate * number) walnuts_to_receive += dig_walnuts_required walnuts_to_collect -= dig_walnuts_required - if OptionName.walnutsanity_repeatables in self.options.walnutsanity: + if WalnutsanityOptionName.repeatables in self.options.walnutsanity: repeatable_walnuts_rate = repeatable_walnuts / total_walnuts repeatable_walnuts_required = round(repeatable_walnuts_rate * number) walnuts_to_receive += repeatable_walnuts_required @@ -104,9 +104,9 @@ def can_get_walnuts(self, number: int) -> StardewRule: return reach_entire_island gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \ - self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ - self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ - self.logic.has(Seed.garlic) & self.can_complete_field_office() + self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ + self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ + self.logic.has(Seed.garlic) & self.can_complete_field_office() @cached_property def can_start_field_office(self) -> StardewRule: @@ -132,4 +132,4 @@ def can_complete_bat_collection(self) -> StardewRule: def can_complete_field_office(self) -> StardewRule: return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ - self.can_complete_frog_collection() & self.can_complete_bat_collection() + self.can_complete_frog_collection() & self.can_complete_bat_collection() diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py deleted file mode 100644 index d0f052348a7e..000000000000 --- a/worlds/stardew_valley/option_groups.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging - -from Options import DeathLink, ProgressionBalancing, Accessibility -from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, - EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression, - ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, - FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations, - QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, - NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, - MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, - FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType, - Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods, Booksanity, Walnutsanity, BundlePlando) - -sv_option_groups = [] -try: - from Options import OptionGroup -except: - logging.warning("Old AP Version, OptionGroup not available.") -else: - sv_option_groups = [ - OptionGroup("General", [ - Goal, - FarmType, - BundleRandomization, - BundlePrice, - EntranceRandomization, - ExcludeGingerIsland, - ]), - OptionGroup("Major Unlocks", [ - SeasonRandomization, - Cropsanity, - BackpackProgression, - ToolProgression, - ElevatorProgression, - SkillProgression, - BuildingProgression, - ]), - OptionGroup("Extra Shuffling", [ - FestivalLocations, - ArcadeMachineLocations, - SpecialOrderLocations, - QuestLocations, - Fishsanity, - Museumsanity, - Friendsanity, - FriendsanityHeartSize, - Monstersanity, - Shipsanity, - Cooksanity, - Chefsanity, - Craftsanity, - Booksanity, - Walnutsanity, - ]), - OptionGroup("Multipliers and Buffs", [ - StartingMoney, - ProfitMargin, - ExperienceMultiplier, - FriendshipMultiplier, - DebrisMultiplier, - NumberOfMovementBuffs, - EnabledFillerBuffs, - TrapItems, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - QuickStart, - ]), - OptionGroup("Advanced Options", [ - Gifting, - DeathLink, - Mods, - BundlePlando, - ProgressionBalancing, - Accessibility, - ]), - ] diff --git a/worlds/stardew_valley/options/__init__.py b/worlds/stardew_valley/options/__init__.py new file mode 100644 index 000000000000..d1436b00dff7 --- /dev/null +++ b/worlds/stardew_valley/options/__init__.py @@ -0,0 +1,6 @@ +from .options import StardewValleyOption, Goal, FarmType, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, \ + SeasonRandomization, Cropsanity, BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, \ + ArcadeMachineLocations, SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, \ + Friendsanity, FriendsanityHeartSize, Booksanity, Walnutsanity, NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, \ + MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, Mods, BundlePlando, \ + StardewValleyOptions diff --git a/worlds/stardew_valley/options/forced_options.py b/worlds/stardew_valley/options/forced_options.py new file mode 100644 index 000000000000..84cdc936b3f1 --- /dev/null +++ b/worlds/stardew_valley/options/forced_options.py @@ -0,0 +1,48 @@ +import logging + +import Options as ap_options +from . import options + +logger = logging.getLogger(__name__) + + +def force_change_options_if_incompatible(world_options: options.StardewValleyOptions, player: int, player_name: str) -> None: + force_ginger_island_inclusion_when_goal_is_ginger_island_related(world_options, player, player_name) + force_walnutsanity_deactivation_when_ginger_island_is_excluded(world_options, player, player_name) + force_accessibility_to_full_when_goal_requires_all_locations(player, player_name, world_options) + + +def force_ginger_island_inclusion_when_goal_is_ginger_island_related(world_options: options.StardewValleyOptions, player: int, player_name: str) -> None: + goal_is_walnut_hunter = world_options.goal == options.Goal.option_greatest_walnut_hunter + goal_is_perfection = world_options.goal == options.Goal.option_perfection + goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection + ginger_island_is_excluded = world_options.exclude_ginger_island == options.ExcludeGingerIsland.option_true + + if goal_is_island_related and ginger_island_is_excluded: + world_options.exclude_ginger_island.value = options.ExcludeGingerIsland.option_false + goal_name = world_options.goal.current_option_name + logger.warning(f"Goal '{goal_name}' requires Ginger Island. " + f"Exclude Ginger Island option forced to 'False' for player {player} ({player_name})") + + +def force_walnutsanity_deactivation_when_ginger_island_is_excluded(world_options: options.StardewValleyOptions, player: int, player_name: str): + ginger_island_is_excluded = world_options.exclude_ginger_island == options.ExcludeGingerIsland.option_true + walnutsanity_is_active = world_options.walnutsanity != options.Walnutsanity.preset_none + + if ginger_island_is_excluded and walnutsanity_is_active: + world_options.walnutsanity.value = options.Walnutsanity.preset_none + logger.warning(f"Walnutsanity requires Ginger Island. " + f"Ginger Island was excluded from {player} ({player_name})'s world, so walnutsanity was force disabled") + + +def force_accessibility_to_full_when_goal_requires_all_locations(player, player_name, world_options): + goal_is_allsanity = world_options.goal == options.Goal.option_allsanity + goal_is_perfection = world_options.goal == options.Goal.option_perfection + goal_requires_all_locations = goal_is_allsanity or goal_is_perfection + accessibility_is_minimal = world_options.accessibility == ap_options.Accessibility.option_minimal + + if goal_requires_all_locations and accessibility_is_minimal: + world_options.accessibility.value = ap_options.Accessibility.option_full + goal_name = world_options.goal.current_option_name + logger.warning(f"Goal '{goal_name}' requires full accessibility. " + f"Accessibility option forced to 'Full' for player {player} ({player_name})") diff --git a/worlds/stardew_valley/options/option_groups.py b/worlds/stardew_valley/options/option_groups.py new file mode 100644 index 000000000000..bcb9bee77ff4 --- /dev/null +++ b/worlds/stardew_valley/options/option_groups.py @@ -0,0 +1,68 @@ +import logging + +import Options as ap_options +from . import options + +sv_option_groups = [] +try: + from Options import OptionGroup +except ImportError: + logging.warning("Old AP Version, OptionGroup not available.") +else: + sv_option_groups = [ + OptionGroup("General", [ + options.Goal, + options.FarmType, + options.BundleRandomization, + options.BundlePrice, + options.EntranceRandomization, + options.ExcludeGingerIsland, + ]), + OptionGroup("Major Unlocks", [ + options.SeasonRandomization, + options.Cropsanity, + options.BackpackProgression, + options.ToolProgression, + options.ElevatorProgression, + options.SkillProgression, + options.BuildingProgression, + ]), + OptionGroup("Extra Shuffling", [ + options.FestivalLocations, + options.ArcadeMachineLocations, + options.SpecialOrderLocations, + options.QuestLocations, + options.Fishsanity, + options.Museumsanity, + options.Friendsanity, + options.FriendsanityHeartSize, + options.Monstersanity, + options.Shipsanity, + options.Cooksanity, + options.Chefsanity, + options.Craftsanity, + options.Booksanity, + options.Walnutsanity, + ]), + OptionGroup("Multipliers and Buffs", [ + options.StartingMoney, + options.ProfitMargin, + options.ExperienceMultiplier, + options.FriendshipMultiplier, + options.DebrisMultiplier, + options.NumberOfMovementBuffs, + options.EnabledFillerBuffs, + options.TrapItems, + options.MultipleDaySleepEnabled, + options.MultipleDaySleepCost, + options.QuickStart, + ]), + OptionGroup("Advanced Options", [ + options.Gifting, + ap_options.DeathLink, + options.Mods, + options.BundlePlando, + ap_options.ProgressionBalancing, + ap_options.Accessibility, + ]), + ] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options/options.py similarity index 97% rename from worlds/stardew_valley/options.py rename to worlds/stardew_valley/options/options.py index 5369e88a2dcb..5d3b25b4da13 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options/options.py @@ -4,9 +4,9 @@ from typing import Protocol, ClassVar from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility -from .mods.mod_data import ModNames -from .strings.ap_names.ap_option_names import OptionName -from .strings.bundle_names import all_cc_bundle_names +from ..mods.mod_data import ModNames +from ..strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName +from ..strings.bundle_names import all_cc_bundle_names class StardewValleyOption(Protocol): @@ -582,8 +582,10 @@ class Walnutsanity(OptionSet): """ internal_name = "walnutsanity" display_name = "Walnutsanity" - valid_keys = frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes, OptionName.walnutsanity_dig_spots, - OptionName.walnutsanity_repeatables, }) + valid_keys = frozenset({ + WalnutsanityOptionName.puzzles, WalnutsanityOptionName.bushes, WalnutsanityOptionName.dig_spots, + WalnutsanityOptionName.repeatables, + }) preset_none = frozenset() preset_all = valid_keys default = preset_none @@ -622,12 +624,14 @@ class EnabledFillerBuffs(OptionSet): """ internal_name = "enabled_filler_buffs" display_name = "Enabled Filler Buffs" - valid_keys = frozenset({OptionName.buff_luck, OptionName.buff_damage, OptionName.buff_defense, OptionName.buff_immunity, OptionName.buff_health, - OptionName.buff_energy, OptionName.buff_bite, OptionName.buff_fish_trap, OptionName.buff_fishing_bar}) - # OptionName.buff_quality, OptionName.buff_glow}) # Disabled these two buffs because they are too hard to make on the mod side + valid_keys = frozenset({ + BuffOptionName.luck, BuffOptionName.damage, BuffOptionName.defense, BuffOptionName.immunity, BuffOptionName.health, + BuffOptionName.energy, BuffOptionName.bite, BuffOptionName.fish_trap, BuffOptionName.fishing_bar, + }) + # OptionName.buff_quality, OptionName.buff_glow}) # Disabled these two buffs because they are too hard to make on the mod side preset_none = frozenset() preset_all = valid_keys - default = frozenset({OptionName.buff_luck, OptionName.buff_defense, OptionName.buff_bite}) + default = frozenset({BuffOptionName.luck, BuffOptionName.defense, BuffOptionName.bite}) class ExcludeGingerIsland(Toggle): @@ -762,7 +766,6 @@ class Gifting(Toggle): ModNames.wellwick, ModNames.shiko, ModNames.delores, ModNames.riley, ModNames.boarding_house} - if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys(): disabled_mods = {} diff --git a/worlds/stardew_valley/options/presets.py b/worlds/stardew_valley/options/presets.py new file mode 100644 index 000000000000..c2c210e5ca6e --- /dev/null +++ b/worlds/stardew_valley/options/presets.py @@ -0,0 +1,371 @@ +from typing import Any, Dict + +import Options as ap_options +from . import options +from ..strings.ap_names.ap_option_names import WalnutsanityOptionName + +# @formatter:off +all_random_settings = { + "progression_balancing": "random", + "accessibility": "random", + options.Goal.internal_name: "random", + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "random", + options.ProfitMargin.internal_name: "random", + options.BundleRandomization.internal_name: "random", + options.BundlePrice.internal_name: "random", + options.EntranceRandomization.internal_name: "random", + options.SeasonRandomization.internal_name: "random", + options.Cropsanity.internal_name: "random", + options.BackpackProgression.internal_name: "random", + options.ToolProgression.internal_name: "random", + options.ElevatorProgression.internal_name: "random", + options.SkillProgression.internal_name: "random", + options.BuildingProgression.internal_name: "random", + options.FestivalLocations.internal_name: "random", + options.ArcadeMachineLocations.internal_name: "random", + options.SpecialOrderLocations.internal_name: "random", + options.QuestLocations.internal_name: "random", + options.Fishsanity.internal_name: "random", + options.Museumsanity.internal_name: "random", + options.Monstersanity.internal_name: "random", + options.Shipsanity.internal_name: "random", + options.Cooksanity.internal_name: "random", + options.Chefsanity.internal_name: "random", + options.Craftsanity.internal_name: "random", + options.Friendsanity.internal_name: "random", + options.FriendsanityHeartSize.internal_name: "random", + options.Booksanity.internal_name: "random", + options.NumberOfMovementBuffs.internal_name: "random", + options.ExcludeGingerIsland.internal_name: "random", + options.TrapItems.internal_name: "random", + options.MultipleDaySleepEnabled.internal_name: "random", + options.MultipleDaySleepCost.internal_name: "random", + options.ExperienceMultiplier.internal_name: "random", + options.FriendshipMultiplier.internal_name: "random", + options.DebrisMultiplier.internal_name: "random", + options.QuickStart.internal_name: "random", + options.Gifting.internal_name: "random", + "death_link": "random", +} + +easy_settings = { + options.Goal.internal_name: options.Goal.option_community_center, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "very rich", + options.ProfitMargin.internal_name: "double", + options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, + options.BundlePrice.internal_name: options.BundlePrice.option_cheap, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive_very_cheap, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla_very_short, + options.QuestLocations.internal_name: "minimum", + options.Fishsanity.internal_name: options.Fishsanity.option_only_easy_fish, + options.Museumsanity.internal_name: options.Museumsanity.option_milestones, + options.Monstersanity.internal_name: options.Monstersanity.option_one_per_category, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_none, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, + options.NumberOfMovementBuffs.internal_name: 8, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_easy, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "free", + options.ExperienceMultiplier.internal_name: "triple", + options.FriendshipMultiplier.internal_name: "quadruple", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_quarter, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "false", +} + +medium_settings = { + options.Goal.internal_name: options.Goal.option_community_center, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "rich", + options.ProfitMargin.internal_name: 150, + options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, + options.BundlePrice.internal_name: options.BundlePrice.option_normal, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_non_progression, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive_cheap, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_cheap, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_victories_easy, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_short, + options.QuestLocations.internal_name: "normal", + options.Fishsanity.internal_name: options.Fishsanity.option_exclude_legendaries, + options.Museumsanity.internal_name: options.Museumsanity.option_milestones, + options.Monstersanity.internal_name: options.Monstersanity.option_one_per_monster, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_queen_of_sauce, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_power_skill, + options.Walnutsanity.internal_name: [WalnutsanityOptionName.puzzles], + options.NumberOfMovementBuffs.internal_name: 6, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_medium, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "free", + options.ExperienceMultiplier.internal_name: "double", + options.FriendshipMultiplier.internal_name: "triple", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_half, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "false", +} + +hard_settings = { + options.Goal.internal_name: options.Goal.option_grandpa_evaluation, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "extra", + options.ProfitMargin.internal_name: "normal", + options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings_without_house, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive_from_previous_floor, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi_short, + options.QuestLocations.internal_name: "lots", + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Shipsanity.internal_name: options.Shipsanity.option_crops, + options.Cooksanity.internal_name: options.Cooksanity.option_queen_of_sauce, + options.Chefsanity.internal_name: options.Chefsanity.option_qos_and_purchases, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_all, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all, + options.NumberOfMovementBuffs.internal_name: 4, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_hard, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "cheap", + options.ExperienceMultiplier.internal_name: "vanilla", + options.FriendshipMultiplier.internal_name: "double", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_vanilla, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "true", +} + +nightmare_settings = { + options.Goal.internal_name: options.Goal.option_community_center, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "vanilla", + options.ProfitMargin.internal_name: "half", + options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, + options.BundlePrice.internal_name: options.BundlePrice.option_very_expensive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive_from_previous_floor, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.QuestLocations.internal_name: "maximum", + options.Fishsanity.internal_name: options.Fishsanity.option_special, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.Monstersanity.internal_name: options.Monstersanity.option_split_goals, + options.Shipsanity.internal_name: options.Shipsanity.option_full_shipment_with_fish, + options.Cooksanity.internal_name: options.Cooksanity.option_queen_of_sauce, + options.Chefsanity.internal_name: options.Chefsanity.option_qos_and_purchases, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all, + options.NumberOfMovementBuffs.internal_name: 2, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_hell, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "expensive", + options.ExperienceMultiplier.internal_name: "half", + options.FriendshipMultiplier.internal_name: "vanilla", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_vanilla, + options.QuickStart.internal_name: options.QuickStart.option_false, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "true", +} + +short_settings = { + options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "filthy rich", + options.ProfitMargin.internal_name: "quadruple", + options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, + options.BundlePrice.internal_name: options.BundlePrice.option_minimum, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive_very_cheap, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla_very_short, + options.QuestLocations.internal_name: "none", + options.Fishsanity.internal_name: options.Fishsanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_none, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, + options.NumberOfMovementBuffs.internal_name: 10, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_easy, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "free", + options.ExperienceMultiplier.internal_name: "quadruple", + options.FriendshipMultiplier.internal_name: 800, + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_none, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "false", +} + +minsanity_settings = { + options.Goal.internal_name: options.Goal.default, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: options.StartingMoney.default, + options.ProfitMargin.internal_name: options.ProfitMargin.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.default, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla_very_short, + options.QuestLocations.internal_name: "none", + options.Fishsanity.internal_name: options.Fishsanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_none, + options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, + options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.default, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.default, + options.MultipleDaySleepCost.internal_name: options.MultipleDaySleepCost.default, + options.ExperienceMultiplier.internal_name: options.ExperienceMultiplier.default, + options.FriendshipMultiplier.internal_name: options.FriendshipMultiplier.default, + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.default, + options.QuickStart.internal_name: options.QuickStart.default, + options.Gifting.internal_name: options.Gifting.default, + "death_link": ap_options.DeathLink.default, +} + +allsanity_settings = { + options.Goal.internal_name: options.Goal.default, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: options.StartingMoney.default, + options.ProfitMargin.internal_name: options.ProfitMargin.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.QuestLocations.internal_name: "maximum", + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Shipsanity.internal_name: options.Shipsanity.option_everything, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + options.Chefsanity.internal_name: options.Chefsanity.option_all, + options.Craftsanity.internal_name: options.Craftsanity.option_all, + options.Friendsanity.internal_name: options.Friendsanity.option_all, + options.FriendsanityHeartSize.internal_name: 1, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all, + options.NumberOfMovementBuffs.internal_name: 12, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.default, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.default, + options.MultipleDaySleepCost.internal_name: options.MultipleDaySleepCost.default, + options.ExperienceMultiplier.internal_name: options.ExperienceMultiplier.default, + options.FriendshipMultiplier.internal_name: options.FriendshipMultiplier.default, + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.default, + options.QuickStart.internal_name: options.QuickStart.default, + options.Gifting.internal_name: options.Gifting.default, + "death_link": ap_options.DeathLink.default, +} +# @formatter:on + + +sv_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Easy": easy_settings, + "Medium": medium_settings, + "Hard": hard_settings, + "Nightmare": nightmare_settings, + "Short": short_settings, + "Minsanity": minsanity_settings, + "Allsanity": allsanity_settings, +} diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py deleted file mode 100644 index 62672f29e424..000000000000 --- a/worlds/stardew_valley/presets.py +++ /dev/null @@ -1,376 +0,0 @@ -from typing import Any, Dict - -from Options import Accessibility, ProgressionBalancing, DeathLink -from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \ - BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \ - SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, ExcludeGingerIsland, TrapItems, \ - MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ - Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Booksanity, Walnutsanity, EnabledFillerBuffs - -# @formatter:off -from .strings.ap_names.ap_option_names import OptionName - -all_random_settings = { - "progression_balancing": "random", - "accessibility": "random", - Goal.internal_name: "random", - FarmType.internal_name: "random", - StartingMoney.internal_name: "random", - ProfitMargin.internal_name: "random", - BundleRandomization.internal_name: "random", - BundlePrice.internal_name: "random", - EntranceRandomization.internal_name: "random", - SeasonRandomization.internal_name: "random", - Cropsanity.internal_name: "random", - BackpackProgression.internal_name: "random", - ToolProgression.internal_name: "random", - ElevatorProgression.internal_name: "random", - SkillProgression.internal_name: "random", - BuildingProgression.internal_name: "random", - FestivalLocations.internal_name: "random", - ArcadeMachineLocations.internal_name: "random", - SpecialOrderLocations.internal_name: "random", - QuestLocations.internal_name: "random", - Fishsanity.internal_name: "random", - Museumsanity.internal_name: "random", - Monstersanity.internal_name: "random", - Shipsanity.internal_name: "random", - Cooksanity.internal_name: "random", - Chefsanity.internal_name: "random", - Craftsanity.internal_name: "random", - Friendsanity.internal_name: "random", - FriendsanityHeartSize.internal_name: "random", - Booksanity.internal_name: "random", - NumberOfMovementBuffs.internal_name: "random", - ExcludeGingerIsland.internal_name: "random", - TrapItems.internal_name: "random", - MultipleDaySleepEnabled.internal_name: "random", - MultipleDaySleepCost.internal_name: "random", - ExperienceMultiplier.internal_name: "random", - FriendshipMultiplier.internal_name: "random", - DebrisMultiplier.internal_name: "random", - QuickStart.internal_name: "random", - Gifting.internal_name: "random", - "death_link": "random", -} - -easy_settings = { - Goal.internal_name: Goal.option_community_center, - FarmType.internal_name: "random", - StartingMoney.internal_name: "very rich", - ProfitMargin.internal_name: "double", - BundleRandomization.internal_name: BundleRandomization.option_thematic, - BundlePrice.internal_name: BundlePrice.option_cheap, - EntranceRandomization.internal_name: EntranceRandomization.option_disabled, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, - FestivalLocations.internal_name: FestivalLocations.option_easy, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, - QuestLocations.internal_name: "minimum", - Fishsanity.internal_name: Fishsanity.option_only_easy_fish, - Museumsanity.internal_name: Museumsanity.option_milestones, - Monstersanity.internal_name: Monstersanity.option_one_per_category, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_none, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - FriendsanityHeartSize.internal_name: 4, - Booksanity.internal_name: Booksanity.option_none, - Walnutsanity.internal_name: Walnutsanity.preset_none, - NumberOfMovementBuffs.internal_name: 8, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.option_easy, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "free", - ExperienceMultiplier.internal_name: "triple", - FriendshipMultiplier.internal_name: "quadruple", - DebrisMultiplier.internal_name: DebrisMultiplier.option_quarter, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "false", -} - -medium_settings = { - Goal.internal_name: Goal.option_community_center, - FarmType.internal_name: "random", - StartingMoney.internal_name: "rich", - ProfitMargin.internal_name: 150, - BundleRandomization.internal_name: BundleRandomization.option_remixed, - BundlePrice.internal_name: BundlePrice.option_normal, - EntranceRandomization.internal_name: EntranceRandomization.option_non_progression, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_short, - QuestLocations.internal_name: "normal", - Fishsanity.internal_name: Fishsanity.option_exclude_legendaries, - Museumsanity.internal_name: Museumsanity.option_milestones, - Monstersanity.internal_name: Monstersanity.option_one_per_monster, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_queen_of_sauce, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_starting_npcs, - FriendsanityHeartSize.internal_name: 4, - Booksanity.internal_name: Booksanity.option_power_skill, - Walnutsanity.internal_name: [OptionName.walnutsanity_puzzles], - NumberOfMovementBuffs.internal_name: 6, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.option_medium, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "free", - ExperienceMultiplier.internal_name: "double", - FriendshipMultiplier.internal_name: "triple", - DebrisMultiplier.internal_name: DebrisMultiplier.option_half, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "false", -} - -hard_settings = { - Goal.internal_name: Goal.option_grandpa_evaluation, - FarmType.internal_name: "random", - StartingMoney.internal_name: "extra", - ProfitMargin.internal_name: "normal", - BundleRandomization.internal_name: BundleRandomization.option_remixed, - BundlePrice.internal_name: BundlePrice.option_expensive, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings_without_house, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi_short, - QuestLocations.internal_name: "lots", - Fishsanity.internal_name: Fishsanity.option_all, - Museumsanity.internal_name: Museumsanity.option_all, - Monstersanity.internal_name: Monstersanity.option_progressive_goals, - Shipsanity.internal_name: Shipsanity.option_crops, - Cooksanity.internal_name: Cooksanity.option_queen_of_sauce, - Chefsanity.internal_name: Chefsanity.option_qos_and_purchases, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - Booksanity.internal_name: Booksanity.option_all, - Walnutsanity.internal_name: Walnutsanity.preset_all, - NumberOfMovementBuffs.internal_name: 4, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.option_hard, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "cheap", - ExperienceMultiplier.internal_name: "vanilla", - FriendshipMultiplier.internal_name: "double", - DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "true", -} - -nightmare_settings = { - Goal.internal_name: Goal.option_community_center, - FarmType.internal_name: "random", - StartingMoney.internal_name: "vanilla", - ProfitMargin.internal_name: "half", - BundleRandomization.internal_name: BundleRandomization.option_shuffled, - BundlePrice.internal_name: BundlePrice.option_very_expensive, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - QuestLocations.internal_name: "maximum", - Fishsanity.internal_name: Fishsanity.option_special, - Museumsanity.internal_name: Museumsanity.option_all, - Monstersanity.internal_name: Monstersanity.option_split_goals, - Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, - Cooksanity.internal_name: Cooksanity.option_queen_of_sauce, - Chefsanity.internal_name: Chefsanity.option_qos_and_purchases, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 4, - Booksanity.internal_name: Booksanity.option_all, - Walnutsanity.internal_name: Walnutsanity.preset_all, - NumberOfMovementBuffs.internal_name: 2, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.option_hell, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "expensive", - ExperienceMultiplier.internal_name: "half", - FriendshipMultiplier.internal_name: "vanilla", - DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, - QuickStart.internal_name: QuickStart.option_false, - Gifting.internal_name: Gifting.option_true, - "death_link": "true", -} - -short_settings = { - Goal.internal_name: Goal.option_bottom_of_the_mines, - FarmType.internal_name: "random", - StartingMoney.internal_name: "filthy rich", - ProfitMargin.internal_name: "quadruple", - BundleRandomization.internal_name: BundleRandomization.option_remixed, - BundlePrice.internal_name: BundlePrice.option_minimum, - EntranceRandomization.internal_name: EntranceRandomization.option_disabled, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Cropsanity.internal_name: Cropsanity.option_disabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, - FestivalLocations.internal_name: FestivalLocations.option_disabled, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, - QuestLocations.internal_name: "none", - Fishsanity.internal_name: Fishsanity.option_none, - Museumsanity.internal_name: Museumsanity.option_none, - Monstersanity.internal_name: Monstersanity.option_none, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_none, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - FriendsanityHeartSize.internal_name: 4, - Booksanity.internal_name: Booksanity.option_none, - Walnutsanity.internal_name: Walnutsanity.preset_none, - NumberOfMovementBuffs.internal_name: 10, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.option_easy, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "free", - ExperienceMultiplier.internal_name: "quadruple", - FriendshipMultiplier.internal_name: 800, - DebrisMultiplier.internal_name: DebrisMultiplier.option_none, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "false", -} - -minsanity_settings = { - Goal.internal_name: Goal.default, - FarmType.internal_name: "random", - StartingMoney.internal_name: StartingMoney.default, - ProfitMargin.internal_name: ProfitMargin.default, - BundleRandomization.internal_name: BundleRandomization.default, - BundlePrice.internal_name: BundlePrice.default, - EntranceRandomization.internal_name: EntranceRandomization.default, - SeasonRandomization.internal_name: SeasonRandomization.option_disabled, - Cropsanity.internal_name: Cropsanity.option_disabled, - BackpackProgression.internal_name: BackpackProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_vanilla, - ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, - SkillProgression.internal_name: SkillProgression.option_vanilla, - BuildingProgression.internal_name: BuildingProgression.option_vanilla, - FestivalLocations.internal_name: FestivalLocations.option_disabled, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, - QuestLocations.internal_name: "none", - Fishsanity.internal_name: Fishsanity.option_none, - Museumsanity.internal_name: Museumsanity.option_none, - Monstersanity.internal_name: Monstersanity.option_none, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_none, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default, - Booksanity.internal_name: Booksanity.option_none, - Walnutsanity.internal_name: Walnutsanity.preset_none, - NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.default, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, - MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, - ExperienceMultiplier.internal_name: ExperienceMultiplier.default, - FriendshipMultiplier.internal_name: FriendshipMultiplier.default, - DebrisMultiplier.internal_name: DebrisMultiplier.default, - QuickStart.internal_name: QuickStart.default, - Gifting.internal_name: Gifting.default, - "death_link": DeathLink.default, -} - -allsanity_settings = { - Goal.internal_name: Goal.default, - FarmType.internal_name: "random", - StartingMoney.internal_name: StartingMoney.default, - ProfitMargin.internal_name: ProfitMargin.default, - BundleRandomization.internal_name: BundleRandomization.default, - BundlePrice.internal_name: BundlePrice.default, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - QuestLocations.internal_name: "maximum", - Fishsanity.internal_name: Fishsanity.option_all, - Museumsanity.internal_name: Museumsanity.option_all, - Monstersanity.internal_name: Monstersanity.option_progressive_goals, - Shipsanity.internal_name: Shipsanity.option_everything, - Cooksanity.internal_name: Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_all, - Craftsanity.internal_name: Craftsanity.option_all, - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 1, - Booksanity.internal_name: Booksanity.option_all, - Walnutsanity.internal_name: Walnutsanity.preset_all, - NumberOfMovementBuffs.internal_name: 12, - EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.default, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, - MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, - ExperienceMultiplier.internal_name: ExperienceMultiplier.default, - FriendshipMultiplier.internal_name: FriendshipMultiplier.default, - DebrisMultiplier.internal_name: DebrisMultiplier.default, - QuickStart.internal_name: QuickStart.default, - Gifting.internal_name: Gifting.default, - "death_link": DeathLink.default, -} -# @formatter:on - - -sv_options_presets: Dict[str, Dict[str, Any]] = { - "All random": all_random_settings, - "Easy": easy_settings, - "Medium": medium_settings, - "Hard": hard_settings, - "Nightmare": nightmare_settings, - "Short": short_settings, - "Minsanity": minsanity_settings, - "Allsanity": allsanity_settings, -} diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 96f081788041..54afc31eb892 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -25,7 +25,7 @@ from .stardew_rule import And, StardewRule, true_ from .stardew_rule.indirect_connection import look_for_indirect_connection from .stardew_rule.rule_explain import explain -from .strings.ap_names.ap_option_names import OptionName +from .strings.ap_names.ap_option_names import WalnutsanityOptionName from .strings.ap_names.community_upgrade_names import CommunityUpgrade from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes from .strings.ap_names.transport_names import Transportation @@ -436,7 +436,7 @@ def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: Sta def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options): - if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: + if WalnutsanityOptionName.puzzles not in world_options.walnutsanity: return MultiWorldRules.add_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut)) @@ -463,14 +463,14 @@ def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_optio def set_walnut_bushes_rules(logic, multiworld, player, world_options): - if OptionName.walnutsanity_bushes not in world_options.walnutsanity: + if WalnutsanityOptionName.bushes not in world_options.walnutsanity: return # I don't think any of the bushes require something special, but that might change with ER return def set_walnut_dig_spot_rules(logic, multiworld, player, world_options): - if OptionName.walnutsanity_dig_spots not in world_options.walnutsanity: + if WalnutsanityOptionName.dig_spots not in world_options.walnutsanity: return for dig_spot_walnut in locations.locations_by_tag[LocationTags.WALNUTSANITY_DIG]: @@ -483,7 +483,7 @@ def set_walnut_dig_spot_rules(logic, multiworld, player, world_options): def set_walnut_repeatable_rules(logic, multiworld, player, world_options): - if OptionName.walnutsanity_repeatables not in world_options.walnutsanity: + if WalnutsanityOptionName.repeatables not in world_options.walnutsanity: return for i in range(1, 6): MultiWorldRules.set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1)) diff --git a/worlds/stardew_valley/strings/ap_names/ap_option_names.py b/worlds/stardew_valley/strings/ap_names/ap_option_names.py index a5cc10f7d7b8..7ff2cc783d11 100644 --- a/worlds/stardew_valley/strings/ap_names/ap_option_names.py +++ b/worlds/stardew_valley/strings/ap_names/ap_option_names.py @@ -1,16 +1,19 @@ -class OptionName: - walnutsanity_puzzles = "Puzzles" - walnutsanity_bushes = "Bushes" - walnutsanity_dig_spots = "Dig Spots" - walnutsanity_repeatables = "Repeatables" - buff_luck = "Luck" - buff_damage = "Damage" - buff_defense = "Defense" - buff_immunity = "Immunity" - buff_health = "Health" - buff_energy = "Energy" - buff_bite = "Bite Rate" - buff_fish_trap = "Fish Trap" - buff_fishing_bar = "Fishing Bar Size" - buff_quality = "Quality" - buff_glow = "Glow" +class WalnutsanityOptionName: + puzzles = "Puzzles" + bushes = "Bushes" + dig_spots = "Dig Spots" + repeatables = "Repeatables" + + +class BuffOptionName: + luck = "Luck" + damage = "Damage" + defense = "Defense" + immunity = "Immunity" + health = "Health" + energy = "Energy" + bite = "Bite Rate" + fish_trap = "Fish Trap" + fishing_bar = "Fishing Bar Size" + quality = "Quality" + glow = "Glow" diff --git a/worlds/stardew_valley/strings/ap_names/mods/__init__.py b/worlds/stardew_valley/strings/ap_names/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/TestBooksanity.py b/worlds/stardew_valley/test/TestBooksanity.py index 3ca52f5728c1..942f35d961a9 100644 --- a/worlds/stardew_valley/test/TestBooksanity.py +++ b/worlds/stardew_valley/test/TestBooksanity.py @@ -1,6 +1,5 @@ from . import SVTestBase from ..options import ExcludeGingerIsland, Booksanity, Shipsanity -from ..strings.ap_names.ap_option_names import OptionName from ..strings.book_names import Book, LostBook power_books = [Book.animal_catalogue, Book.book_of_mysteries, diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 9db7f06ff5a5..2cd83f013ae5 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,6 +1,6 @@ import itertools -from Options import NamedRange, Accessibility +from Options import NamedRange from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld from .assertion import WorldAssertMixin from .long.option_names import all_option_choices @@ -54,23 +54,6 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): victory = multi_world.find_item("Victory", 1) self.assertEqual(victory.name, location) - def test_given_perfection_goal_when_generate_then_accessibility_is_forced_to_full(self): - """There is a bug with the current victory condition of the perfection goal that can create unwinnable seeds if the accessibility is set to minimal and - the world gets flooded with progression items through plando. This will increase the amount of collected progression items pass the total amount - calculated for the world when creating the item pool. This will cause the victory condition to be met before all locations are collected, so some could - be left inaccessible, which in practice will make the seed unwinnable. - """ - for accessibility in Accessibility.options.keys(): - world_options = {Goal.internal_name: Goal.option_perfection, "accessibility": accessibility} - with self.solo_world_sub_test(f"Accessibility: {accessibility}", world_options) as (_, world): - self.assertEqual(world.options.accessibility, Accessibility.option_full) - - def test_given_allsanity_goal_when_generate_then_accessibility_is_forced_to_full(self): - for accessibility in Accessibility.options.keys(): - world_options = {Goal.internal_name: Goal.option_allsanity, "accessibility": accessibility} - with self.solo_world_sub_test(f"Accessibility: {accessibility}", world_options) as (_, world): - self.assertEqual(world.options.accessibility, Accessibility.option_full) - class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): @@ -144,7 +127,7 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase): - def test_given_choice_when_generate_exclude_ginger_island(self): + def test_given_choice_when_generate_exclude_ginger_island_then_ginger_island_is_properly_excluded(self): for option, option_choice in all_option_choices: if option is ExcludeGingerIsland: continue @@ -163,19 +146,6 @@ def test_given_choice_when_generate_exclude_ginger_island(self): self.assert_basic_checks(multiworld) self.assert_no_ginger_island_content(multiworld) - def test_given_island_related_goal_then_override_exclude_ginger_island(self): - island_goals = ["greatest_walnut_hunter", "perfection"] - for goal, exclude_island in itertools.product(island_goals, ExcludeGingerIsland.options): - world_options = { - Goal: goal, - ExcludeGingerIsland: exclude_island - } - - with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options) \ - as (multiworld, stardew_world): - self.assertEqual(stardew_world.options.exclude_ginger_island, ExcludeGingerIsland.option_false) - self.assert_basic_checks(multiworld) - class TestTraps(SVTestCase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): diff --git a/worlds/stardew_valley/test/TestOptionsPairs.py b/worlds/stardew_valley/test/TestOptionsPairs.py index d953696e887d..d489ab1ff282 100644 --- a/worlds/stardew_valley/test/TestOptionsPairs.py +++ b/worlds/stardew_valley/test/TestOptionsPairs.py @@ -1,13 +1,12 @@ from . import SVTestBase from .assertion import WorldAssertMixin from .. import options -from ..options import Goal, QuestLocations class TestCrypticNoteNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_cryptic_note, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_cryptic_note, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -16,8 +15,8 @@ def test_given_option_pair_then_basic_checks(self): class TestCompleteCollectionNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_complete_collection, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_complete_collection, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -26,8 +25,8 @@ def test_given_option_pair_then_basic_checks(self): class TestProtectorOfTheValleyNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_protector_of_the_valley, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_protector_of_the_valley, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -36,8 +35,8 @@ def test_given_option_pair_then_basic_checks(self): class TestCraftMasterNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_craft_master, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_craft_master, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -46,7 +45,7 @@ def test_given_option_pair_then_basic_checks(self): class TestCraftMasterNoSpecialOrder(WorldAssertMixin, SVTestBase): options = { - options.Goal.internal_name: Goal.option_craft_master, + options.Goal.internal_name: options.Goal.option_craft_master, options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.alias_disabled, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, options.Craftsanity.internal_name: options.Craftsanity.option_none diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index c2e962d88a7e..bd1b67297473 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -3,7 +3,8 @@ from typing import Set from BaseClasses import get_seed -from . import SVTestCase, complete_options_with_default +from . import SVTestCase +from .options.utils import fill_dataclass_with_default from .. import create_content from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions @@ -59,7 +60,7 @@ def test_entrance_randomization(self): (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ EntranceRandomization.internal_name: option, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, @@ -87,7 +88,7 @@ def test_entrance_randomization_without_island(self): (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ EntranceRandomization.internal_name: option, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, @@ -116,7 +117,7 @@ def test_entrance_randomization_without_island(self): f"Connections are duplicated in randomization.") def test_cannot_put_island_access_on_island(self): - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ EntranceRandomization.internal_name: EntranceRandomization.option_buildings, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py index e1ab348def41..c1e8c2c8f095 100644 --- a/worlds/stardew_valley/test/TestWalnutsanity.py +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -1,6 +1,6 @@ from . import SVTestBase from ..options import ExcludeGingerIsland, Walnutsanity -from ..strings.ap_names.ap_option_names import OptionName +from ..strings.ap_names.ap_option_names import WalnutsanityOptionName class TestWalnutsanityNone(SVTestBase): @@ -49,7 +49,7 @@ def test_logic_received_walnuts(self): class TestWalnutsanityPuzzles(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, - Walnutsanity: frozenset({OptionName.walnutsanity_puzzles}), + Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}), } def test_only_puzzle_walnut_locations(self): @@ -90,7 +90,7 @@ def test_field_office_locations_require_professor_snail(self): class TestWalnutsanityBushes(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, - Walnutsanity: frozenset({OptionName.walnutsanity_bushes}), + Walnutsanity: frozenset({WalnutsanityOptionName.bushes}), } def test_only_bush_walnut_locations(self): @@ -108,7 +108,7 @@ def test_only_bush_walnut_locations(self): class TestWalnutsanityPuzzlesAndBushes(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, - Walnutsanity: frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes}), + Walnutsanity: frozenset({WalnutsanityOptionName.puzzles, WalnutsanityOptionName.bushes}), } def test_only_bush_walnut_locations(self): @@ -136,7 +136,7 @@ def test_logic_received_walnuts(self): class TestWalnutsanityDigSpots(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, - Walnutsanity: frozenset({OptionName.walnutsanity_dig_spots}), + Walnutsanity: frozenset({WalnutsanityOptionName.dig_spots}), } def test_only_dig_spots_walnut_locations(self): @@ -154,7 +154,7 @@ def test_only_dig_spots_walnut_locations(self): class TestWalnutsanityRepeatables(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, - Walnutsanity: frozenset({OptionName.walnutsanity_repeatables}), + Walnutsanity: frozenset({WalnutsanityOptionName.repeatables}), } def test_only_repeatable_walnut_locations(self): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 1a312e569d11..de0ed97882e3 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -2,18 +2,17 @@ import os import threading import unittest -from argparse import Namespace from contextlib import contextmanager from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item -from Options import VerifyKeys +from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all from .assertion import RuleAssertMixin +from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default from .. import StardewValleyWorld, options, StardewItem -from ..options import StardewValleyOptions, StardewValleyOption +from ..options import StardewValleyOption logger = logging.getLogger(__name__) @@ -360,15 +359,7 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp multiworld = setup_base_solo_multiworld(StardewValleyWorld, (), seed=seed) # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test - args = Namespace() - for name, option in StardewValleyWorld.options_dataclass.type_hints.items(): - value = option.from_any(test_options.get(name, option.default)) - - if issubclass(option, VerifyKeys): - # Values should already be verified, but just in case... - value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses) - - setattr(args, name, {1: value}) + args = fill_namespace_with_default(test_options) multiworld.set_options(args) if "start_inventory" in test_options: @@ -388,24 +379,6 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp return multiworld -def parse_class_option_keys(test_options: Optional[Dict]) -> dict: - """ Now the option class is allowed as key. """ - if test_options is None: - return {} - parsed_options = {} - - for option, value in test_options.items(): - if hasattr(option, "internal_name"): - assert option.internal_name not in test_options, "Defined two times by class and internal_name" - parsed_options[option.internal_name] = value - else: - assert option in StardewValleyOptions.type_hints, \ - f"All keys of world_options must be a possible Stardew Valley option, {option} is not." - parsed_options[option] = value - - return parsed_options - - def search_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset) -> Optional[MultiWorld]: try: return cache[frozen_options] @@ -421,16 +394,6 @@ def add_to_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: froze cache[frozen_options] = multi_world -def complete_options_with_default(options_to_complete=None) -> StardewValleyOptions: - if options_to_complete is None: - options_to_complete = {} - - for name, option in StardewValleyOptions.type_hints.items(): - options_to_complete[name] = option.from_any(options_to_complete.get(name, option.default)) - - return StardewValleyOptions(**options_to_complete) - - def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) -> MultiWorld: # noqa if test_options is None: test_options = [] @@ -442,22 +405,10 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) - for i in range(1, len(test_options) + 1): multiworld.game[i] = StardewValleyWorld.game multiworld.player_name.update({i: f"Tester{i}"}) - args = create_args(test_options) + args = fill_namespace_with_default(test_options) multiworld.set_options(args) for step in gen_steps: call_all(multiworld, step) return multiworld - - -def create_args(test_options): - args = Namespace() - for name, option in StardewValleyWorld.options_dataclass.type_hints.items(): - options = {} - for i in range(1, len(test_options) + 1): - player_options = test_options[i - 1] - value = option(player_options[name]) if name in player_options else option.from_any(option.default) - options.update({i: value}) - setattr(args, name, options) - return args diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 56138cf582a7..89f82870e4a7 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -1,7 +1,8 @@ import random from BaseClasses import get_seed -from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld +from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, \ + fill_dataclass_with_default from ..assertion import ModAssertMixin, WorldAssertMixin from ... import items, Group, ItemClassification, create_content from ... import options @@ -122,7 +123,7 @@ def test_mod_entrance_randomization(self): (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), (options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ options.EntranceRandomization.internal_name: option, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, diff --git a/worlds/stardew_valley/test/options/TestForcedOptions.py b/worlds/stardew_valley/test/options/TestForcedOptions.py new file mode 100644 index 000000000000..4c8f0f42c389 --- /dev/null +++ b/worlds/stardew_valley/test/options/TestForcedOptions.py @@ -0,0 +1,84 @@ +import itertools +import unittest + +import Options as ap_options +from .utils import fill_dataclass_with_default +from ... import options +from ...options.forced_options import force_change_options_if_incompatible + + +class TestGoalsRequiringAllLocationsOverrideAccessibility(unittest.TestCase): + + def test_given_goal_requiring_all_locations_when_generate_then_accessibility_is_forced_to_full(self): + """There is a bug with the current victory condition of the perfection goal that can create unwinnable seeds if the accessibility is set to minimal and + the world gets flooded with progression items through plando. This will increase the amount of collected progression items pass the total amount + calculated for the world when creating the item pool. This will cause the victory condition to be met before all locations are collected, so some could + be left inaccessible, which in practice will make the seed unwinnable. + """ + + for goal in [options.Goal.option_perfection, options.Goal.option_allsanity]: + for accessibility in ap_options.Accessibility.options.keys(): + with self.subTest(f"Goal: {options.Goal.get_option_name(goal)} Accessibility: {accessibility}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + "accessibility": accessibility + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.accessibility.value, ap_options.Accessibility.option_full) + + +class TestGingerIslandRelatedGoalsOverrideGingerIslandExclusion(unittest.TestCase): + + def test_given_island_related_goal_when_generate_then_override_exclude_ginger_island(self): + for goal in [options.Goal.option_greatest_walnut_hunter, options.Goal.option_perfection]: + for exclude_island in options.ExcludeGingerIsland.options: + with self.subTest(f"Goal: {options.Goal.get_option_name(goal)} Exclude Ginger Island: {exclude_island}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + options.ExcludeGingerIsland: exclude_island + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.exclude_ginger_island.value, options.ExcludeGingerIsland.option_false) + + +class TestGingerIslandExclusionOverridesWalnutsanity(unittest.TestCase): + + def test_given_ginger_island_excluded_when_generate_then_walnutsanity_is_forced_disabled(self): + walnutsanity_options = options.Walnutsanity.valid_keys + for walnutsanity in ( + walnutsanity + for r in range(len(walnutsanity_options) + 1) + for walnutsanity in itertools.combinations(walnutsanity_options, r) + ): + with self.subTest(f"Walnutsanity: {walnutsanity}"): + world_options = fill_dataclass_with_default({ + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Walnutsanity: walnutsanity + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.walnutsanity.value, options.Walnutsanity.preset_none) + + def test_given_ginger_island_related_goal_and_ginger_island_excluded_when_generate_then_walnutsanity_is_not_changed(self): + for goal in [options.Goal.option_greatest_walnut_hunter, options.Goal.option_perfection]: + walnutsanity_options = options.Walnutsanity.valid_keys + for original_walnutsanity_choice in ( + set(walnutsanity) + for r in range(len(walnutsanity_options) + 1) + for walnutsanity in itertools.combinations(walnutsanity_options, r) + ): + with self.subTest(f"Goal: {options.Goal.get_option_name(goal)} Walnutsanity: {original_walnutsanity_choice}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Walnutsanity: original_walnutsanity_choice + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.walnutsanity.value, original_walnutsanity_choice) diff --git a/worlds/stardew_valley/test/TestPresets.py b/worlds/stardew_valley/test/options/TestPresets.py similarity index 86% rename from worlds/stardew_valley/test/TestPresets.py rename to worlds/stardew_valley/test/options/TestPresets.py index 2bb1c7fbaeaf..9384acd77060 100644 --- a/worlds/stardew_valley/test/TestPresets.py +++ b/worlds/stardew_valley/test/options/TestPresets.py @@ -1,9 +1,7 @@ -import builtins -import inspect - from Options import PerGameCommonOptions, OptionSet -from . import SVTestCase -from .. import sv_options_presets, StardewValleyOptions +from .. import SVTestCase +from ...options import StardewValleyOptions +from ...options.presets import sv_options_presets class TestPresets(SVTestCase): @@ -18,4 +16,4 @@ def test_all_presets_explicitly_set_all_options(self): with self.subTest(f"{preset_name}"): for option_name in mandatory_option_names: with self.subTest(f"{preset_name} -> {option_name}"): - self.assertIn(option_name, sv_options_presets[preset_name]) \ No newline at end of file + self.assertIn(option_name, sv_options_presets[preset_name]) diff --git a/worlds/stardew_valley/test/options/__init__.py b/worlds/stardew_valley/test/options/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/options/utils.py b/worlds/stardew_valley/test/options/utils.py new file mode 100644 index 000000000000..9f02105da84f --- /dev/null +++ b/worlds/stardew_valley/test/options/utils.py @@ -0,0 +1,68 @@ +from argparse import Namespace +from typing import Any, Iterable + +from BaseClasses import PlandoOptions +from Options import VerifyKeys +from ... import StardewValleyWorld +from ...options import StardewValleyOptions, StardewValleyOption + + +def parse_class_option_keys(test_options: dict[str | StardewValleyOption, Any] | None) -> dict: + """ Now the option class is allowed as key. """ + if test_options is None: + return {} + parsed_options = {} + + for option, value in test_options.items(): + if hasattr(option, "internal_name"): + assert option.internal_name not in test_options, "Defined two times by class and internal_name" + parsed_options[option.internal_name] = value + else: + assert option in StardewValleyOptions.type_hints, \ + f"All keys of world_options must be a possible Stardew Valley option, {option} is not." + parsed_options[option] = value + + return parsed_options + + +def fill_dataclass_with_default(test_options: dict[str | StardewValleyOption, Any] | None) -> StardewValleyOptions: + test_options = parse_class_option_keys(test_options) + + filled_options = {} + for option_name, option_class in StardewValleyOptions.type_hints.items(): + + value = option_class.from_any(test_options.get(option_name, option_class.default)) + + if issubclass(option_class, VerifyKeys): + # Values should already be verified, but just in case... + value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses) + + filled_options[option_name] = value + + return StardewValleyOptions(**filled_options) + + +def fill_namespace_with_default(test_options: dict[str, Any] | Iterable[dict[str, Any]]) -> Namespace: + if isinstance(test_options, dict): + test_options = [test_options] + + args = Namespace() + for option_name, option_class in StardewValleyOptions.type_hints.items(): + all_players_option = {} + + for player_id, player_options in enumerate(test_options): + # Player id starts at 1 + player_id += 1 + player_name = f"Tester{player_id}" + + value = option_class.from_any(player_options.get(option_name, option_class.default)) + + if issubclass(option_class, VerifyKeys): + # Values should already be verified, but just in case... + value.verify(StardewValleyWorld, player_name, PlandoOptions.bosses) + + all_players_option[player_id] = value + + setattr(args, option_name, all_players_option) + + return args diff --git a/worlds/stardew_valley/test/stability/TestUniversalTracker.py b/worlds/stardew_valley/test/stability/TestUniversalTracker.py index 3e334098341d..4655b37adf07 100644 --- a/worlds/stardew_valley/test/stability/TestUniversalTracker.py +++ b/worlds/stardew_valley/test/stability/TestUniversalTracker.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import Mock -from .. import SVTestBase, create_args, allsanity_mods_6_x_x +from .. import SVTestBase, allsanity_mods_6_x_x, fill_namespace_with_default from ... import STARDEW_VALLEY, FarmType, BundleRandomization, EntranceRandomization @@ -29,7 +29,7 @@ def test_all_locations_and_items_are_the_same_between_two_generations(self): fake_context = Mock() fake_context.re_gen_passthrough = {STARDEW_VALLEY: ut_data} - args = create_args({0: self.options}) + args = fill_namespace_with_default({0: self.options}) args.outputpath = None args.outputname = None args.multi = 1 From aa22b62b41226b62734988da0b5dcd6f5bff7a33 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:17:25 -0500 Subject: [PATCH 102/112] Stardew Valley: Force deactivation of Mr. Qi's special orders when ginger island is deactivated (#4348) --- .../stardew_valley/options/forced_options.py | 12 +++++++ .../test/options/TestForcedOptions.py | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/worlds/stardew_valley/options/forced_options.py b/worlds/stardew_valley/options/forced_options.py index 84cdc936b3f1..7429f3cbfc65 100644 --- a/worlds/stardew_valley/options/forced_options.py +++ b/worlds/stardew_valley/options/forced_options.py @@ -9,6 +9,7 @@ def force_change_options_if_incompatible(world_options: options.StardewValleyOptions, player: int, player_name: str) -> None: force_ginger_island_inclusion_when_goal_is_ginger_island_related(world_options, player, player_name) force_walnutsanity_deactivation_when_ginger_island_is_excluded(world_options, player, player_name) + force_qi_special_orders_deactivation_when_ginger_island_is_excluded(world_options, player, player_name) force_accessibility_to_full_when_goal_requires_all_locations(player, player_name, world_options) @@ -35,6 +36,17 @@ def force_walnutsanity_deactivation_when_ginger_island_is_excluded(world_options f"Ginger Island was excluded from {player} ({player_name})'s world, so walnutsanity was force disabled") +def force_qi_special_orders_deactivation_when_ginger_island_is_excluded(world_options: options.StardewValleyOptions, player: int, player_name: str): + ginger_island_is_excluded = world_options.exclude_ginger_island == options.ExcludeGingerIsland.option_true + qi_board_is_active = world_options.special_order_locations.value & options.SpecialOrderLocations.value_qi + + if ginger_island_is_excluded and qi_board_is_active: + original_option_name = world_options.special_order_locations.current_option_name + world_options.special_order_locations.value -= options.SpecialOrderLocations.value_qi + logger.warning(f"Mr. Qi's Special Orders requires Ginger Island. " + f"Ginger Island was excluded from {player} ({player_name})'s world, so Special Order Locations was changed from {original_option_name} to {world_options.special_order_locations.current_option_name}") + + def force_accessibility_to_full_when_goal_requires_all_locations(player, player_name, world_options): goal_is_allsanity = world_options.goal == options.Goal.option_allsanity goal_is_perfection = world_options.goal == options.Goal.option_perfection diff --git a/worlds/stardew_valley/test/options/TestForcedOptions.py b/worlds/stardew_valley/test/options/TestForcedOptions.py index 4c8f0f42c389..c32def6c6ca8 100644 --- a/worlds/stardew_valley/test/options/TestForcedOptions.py +++ b/worlds/stardew_valley/test/options/TestForcedOptions.py @@ -82,3 +82,34 @@ def test_given_ginger_island_related_goal_and_ginger_island_excluded_when_genera force_change_options_if_incompatible(world_options, 1, "Tester") self.assertEqual(world_options.walnutsanity.value, original_walnutsanity_choice) + + +class TestGingerIslandExclusionOverridesQisSpecialOrders(unittest.TestCase): + + def test_given_ginger_island_excluded_when_generate_then_qis_special_orders_are_forced_disabled(self): + special_order_options = options.SpecialOrderLocations.options + for special_order in special_order_options.keys(): + with self.subTest(f"Special order: {special_order}"): + world_options = fill_dataclass_with_default({ + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.SpecialOrderLocations: special_order + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.special_order_locations.value & options.SpecialOrderLocations.value_qi, 0) + + def test_given_ginger_island_related_goal_and_ginger_island_excluded_when_generate_then_special_orders_is_not_changed(self): + for goal in [options.Goal.option_greatest_walnut_hunter, options.Goal.option_perfection]: + special_order_options = options.SpecialOrderLocations.options + for special_order, original_special_order_value in special_order_options.items(): + with self.subTest(f"Special order: {special_order}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.SpecialOrderLocations: special_order + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.special_order_locations.value, original_special_order_value) From 0b3d34ab24ffab023800e6230ca95e840cdcb683 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:25:09 +0100 Subject: [PATCH 103/112] CI: update scan-build to v19 (#4338) --- .github/workflows/scan-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml index 5234d862b4d3..ac842070625f 100644 --- a/.github/workflows/scan-build.yml +++ b/.github/workflows/scan-build.yml @@ -40,10 +40,10 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 17 + sudo ./llvm.sh 19 - name: Install scan-build command run: | - sudo apt install clang-tools-17 + sudo apt install clang-tools-19 - name: Get a recent python uses: actions/setup-python@v5 with: @@ -56,7 +56,7 @@ jobs: - name: scan-build run: | source venv/bin/activate - scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y + scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y - name: Store report if: failure() uses: actions/upload-artifact@v4 From 4a5ba756b6d0d26b40a8c3ca5bb3ea3e1ed931b3 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:44:41 +0100 Subject: [PATCH 104/112] WebHost: Set Generator memory limit to 4GiB (#4319) * WebHost: Set Generator memory limit to 4GiB * WebHost: make generator memory limit configurable, better naming * Update WebHostLib/__init__.py Co-authored-by: Fabian Dill * Update docs/webhost configuration sample.yaml --------- Co-authored-by: Fabian Dill --- WebHostLib/__init__.py | 2 ++ WebHostLib/autolauncher.py | 21 ++++++++++++++++++--- docs/webhost configuration sample.yaml | 10 ++++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 9b2b6736f13c..9c713419c986 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -39,6 +39,8 @@ app.config["JOB_THRESHOLD"] = 1 # after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. app.config["JOB_TIME"] = 600 +# memory limit for generator processes in bytes +app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 app.config['SESSION_PERMANENT'] = True # waitress uses one thread for I/O, these are for processing of views that then get sent diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 08a1309ebc73..8ba093e014c5 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -6,6 +6,7 @@ import typing from datetime import timedelta, datetime from threading import Event, Thread +from typing import Any from uuid import UUID from pony.orm import db_session, select, commit @@ -53,7 +54,21 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): generation.state = STATE_STARTED -def init_db(pony_config: dict): +def init_generator(config: dict[str, Any]) -> None: + try: + import resource + except ModuleNotFoundError: + pass # unix only module + else: + # set soft limit for memory to from config (default 4GiB) + soft_limit = config["GENERATOR_MEMORY_LIMIT"] + old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS) + if soft_limit != old_limit: + resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit)) + logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}") + del resource, soft_limit, hard_limit + + pony_config = config["PONY"] db.bind(**pony_config) db.generate_mapping() @@ -105,8 +120,8 @@ def keep_running(): try: with Locker("autogen"): - with multiprocessing.Pool(config["GENERATORS"], initializer=init_db, - initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool: + with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator, + initargs=(config,), maxtasksperchild=10) as generator_pool: with db_session: to_start = select(generation for generation in Generation if generation.state == STATE_STARTED) diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index afb87b399643..93094f1ce73f 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -27,8 +27,14 @@ # If you wish to deploy, uncomment the following line and set it to something not easily guessable. # SECRET_KEY: "Your secret key here" -# TODO -#JOB_THRESHOLD: 2 +# Slot limit to post a generation to Generator process pool instead of rolling directly in WebHost process +#JOB_THRESHOLD: 1 + +# After what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. +#JOB_TIME: 600 + +# Memory limit for Generator processes in bytes, -1 for unlimited. Currently only works on Linux. +#GENERATOR_MEMORY_LIMIT: 4294967296 # waitress uses one thread for I/O, these are for processing of view that get sent #WAITRESS_THREADS: 10 From f79657b41a8aa7904193331b7ae181e0ff9ab967 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 10 Dec 2024 19:53:42 +0100 Subject: [PATCH 105/112] WebHost: disable abbreviations for argparse (#4352) --- WebHost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHost.py b/WebHost.py index 3790a5f6f4d2..768eeb512289 100644 --- a/WebHost.py +++ b/WebHost.py @@ -34,7 +34,7 @@ def get_app() -> "Flask": app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") # inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it. - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument('--config_override', default=None, help="Path to yaml config file that overrules config.yaml.") args = parser.parse_known_args()[0] From 3fb0b57d19b9c223107308c84605e05e6b16e1cf Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:09:36 +0100 Subject: [PATCH 106/112] Core: fix exceptions coming from LocationStore (#4358) * Speedups: add instructions for ASAN * Speedups: move typevars out of classes * Speedups, NetUtils: raise correct exceptions * Speedups: double-check malloc * Tests: more LocationStore tests --- NetUtils.py | 2 ++ _speedups.pyx | 43 ++++++++++++++++++---------- _speedups.pyxbld | 18 ++++++++---- test/netutils/test_location_store.py | 41 ++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 20 deletions(-) diff --git a/NetUtils.py b/NetUtils.py index ec6ff3eb1d81..196a030f4969 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -410,6 +410,8 @@ def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int] checked = state[team, slot] if not checked: # This optimizes the case where everyone connects to a fresh game at the same time. + if slot not in self: + raise KeyError(slot) return [] return [location_id for location_id in self[slot] if diff --git a/_speedups.pyx b/_speedups.pyx index dc039e336500..2ad1a2953a2b 100644 --- a/_speedups.pyx +++ b/_speedups.pyx @@ -69,6 +69,14 @@ cdef struct IndexEntry: size_t count +if TYPE_CHECKING: + State = Dict[Tuple[int, int], Set[int]] +else: + State = Union[Tuple[int, int], Set[int], defaultdict] + +T = TypeVar('T') + + @cython.auto_pickle(False) cdef class LocationStore: """Compact store for locations and their items in a MultiServer""" @@ -137,10 +145,16 @@ cdef class LocationStore: warnings.warn("Game has no locations") # allocate the arrays and invalidate index (0xff...) - self.entries = self._mem.alloc(count, sizeof(LocationEntry)) + if count: + # leaving entries as NULL if there are none, makes potential memory errors more visible + self.entries = self._mem.alloc(count, sizeof(LocationEntry)) self.sender_index = self._mem.alloc(max_sender + 1, sizeof(IndexEntry)) self._raw_proxies = self._mem.alloc(max_sender + 1, sizeof(PyObject*)) + assert (not self.entries) == (not count) + assert self.sender_index + assert self._raw_proxies + # build entries and index cdef size_t i = 0 for sender, locations in sorted(locations_dict.items()): @@ -190,8 +204,6 @@ cdef class LocationStore: raise KeyError(key) return self._raw_proxies[key] - T = TypeVar('T') - def get(self, key: int, default: T) -> Union[PlayerLocationProxy, T]: # calling into self.__getitem__ here is slow, but this is not used in MultiServer try: @@ -246,12 +258,11 @@ cdef class LocationStore: all_locations[sender].add(entry.location) return all_locations - if TYPE_CHECKING: - State = Dict[Tuple[int, int], Set[int]] - else: - State = Union[Tuple[int, int], Set[int], defaultdict] - def get_checked(self, state: State, team: int, slot: int) -> List[int]: + cdef ap_player_t sender = slot + if sender < 0 or sender >= self.sender_index_size: + raise KeyError(slot) + # This used to validate checks actually exist. A remnant from the past. # If the order of locations becomes relevant at some point, we could not do sorted(set), so leaving it. cdef set checked = state[team, slot] @@ -263,7 +274,6 @@ cdef class LocationStore: # Unless the set is close to empty, it's cheaper to use the python set directly, so we do that. cdef LocationEntry* entry - cdef ap_player_t sender = slot cdef size_t start = self.sender_index[sender].start cdef size_t count = self.sender_index[sender].count return [entry.location for @@ -273,9 +283,11 @@ cdef class LocationStore: def get_missing(self, state: State, team: int, slot: int) -> List[int]: cdef LocationEntry* entry cdef ap_player_t sender = slot + if sender < 0 or sender >= self.sender_index_size: + raise KeyError(slot) + cdef set checked = state[team, slot] cdef size_t start = self.sender_index[sender].start cdef size_t count = self.sender_index[sender].count - cdef set checked = state[team, slot] if not len(checked): # Skip `in` if none have been checked. # This optimizes the case where everyone connects to a fresh game at the same time. @@ -290,9 +302,11 @@ cdef class LocationStore: def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]: cdef LocationEntry* entry cdef ap_player_t sender = slot + if sender < 0 or sender >= self.sender_index_size: + raise KeyError(slot) + cdef set checked = state[team, slot] cdef size_t start = self.sender_index[sender].start cdef size_t count = self.sender_index[sender].count - cdef set checked = state[team, slot] return sorted([(entry.receiver, entry.item) for entry in self.entries[start:start+count] if entry.location not in checked]) @@ -328,7 +342,8 @@ cdef class PlayerLocationProxy: cdef LocationEntry* entry = NULL # binary search cdef size_t l = self._store.sender_index[self._player].start - cdef size_t r = l + self._store.sender_index[self._player].count + cdef size_t e = l + self._store.sender_index[self._player].count + cdef size_t r = e cdef size_t m while l < r: m = (l + r) // 2 @@ -337,7 +352,7 @@ cdef class PlayerLocationProxy: l = m + 1 else: r = m - if entry: # count != 0 + if l < e: entry = self._store.entries + l if entry.location == loc: return entry @@ -349,8 +364,6 @@ cdef class PlayerLocationProxy: return entry.item, entry.receiver, entry.flags raise KeyError(f"No location {key} for player {self._player}") - T = TypeVar('T') - def get(self, key: int, default: T) -> Union[Tuple[int, int, int], T]: cdef LocationEntry* entry = self._get(key) if entry: diff --git a/_speedups.pyxbld b/_speedups.pyxbld index 974eaed03b6a..98f9734614cc 100644 --- a/_speedups.pyxbld +++ b/_speedups.pyxbld @@ -3,8 +3,16 @@ import os def make_ext(modname, pyxfilename): from distutils.extension import Extension - return Extension(name=modname, - sources=[pyxfilename], - depends=["intset.h"], - include_dirs=[os.getcwd()], - language="c") + return Extension( + name=modname, + sources=[pyxfilename], + depends=["intset.h"], + include_dirs=[os.getcwd()], + language="c", + # to enable ASAN and debug build: + # extra_compile_args=["-fsanitize=address", "-UNDEBUG", "-Og", "-g"], + # extra_objects=["-fsanitize=address"], + # NOTE: we can not put -lasan at the front of link args, so needs to be run with + # LD_PRELOAD=/usr/lib/libasan.so ASAN_OPTIONS=detect_leaks=0 path/to/exe + # NOTE: this can't find everything unless libpython and cymem are also built with ASAN + ) diff --git a/test/netutils/test_location_store.py b/test/netutils/test_location_store.py index 1b984015844d..264f35b3cc65 100644 --- a/test/netutils/test_location_store.py +++ b/test/netutils/test_location_store.py @@ -115,6 +115,7 @@ def test_find_item(self) -> None: def test_get_for_player(self) -> None: self.assertEqual(self.store.get_for_player(3), {4: {9}}) self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}}) + self.assertEqual(self.store.get_for_player(9999), {}) def test_get_checked(self) -> None: self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13]) @@ -122,18 +123,48 @@ def test_get_checked(self) -> None: self.assertEqual(self.store.get_checked(empty_state, 0, 1), []) self.assertEqual(self.store.get_checked(full_state, 0, 3), [9]) + def test_get_checked_exception(self) -> None: + with self.assertRaises(KeyError): + self.store.get_checked(empty_state, 0, 9999) + bad_state = {(0, 6): {1}} + with self.assertRaises(KeyError): + self.store.get_checked(bad_state, 0, 6) + bad_state = {(0, 9999): set()} + with self.assertRaises(KeyError): + self.store.get_checked(bad_state, 0, 9999) + def test_get_missing(self) -> None: self.assertEqual(self.store.get_missing(full_state, 0, 1), []) self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13]) self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13]) self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9]) + def test_get_missing_exception(self) -> None: + with self.assertRaises(KeyError): + self.store.get_missing(empty_state, 0, 9999) + bad_state = {(0, 6): {1}} + with self.assertRaises(KeyError): + self.store.get_missing(bad_state, 0, 6) + bad_state = {(0, 9999): set()} + with self.assertRaises(KeyError): + self.store.get_missing(bad_state, 0, 9999) + def test_get_remaining(self) -> None: self.assertEqual(self.store.get_remaining(full_state, 0, 1), []) self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)]) self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)]) self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)]) + def test_get_remaining_exception(self) -> None: + with self.assertRaises(KeyError): + self.store.get_remaining(empty_state, 0, 9999) + bad_state = {(0, 6): {1}} + with self.assertRaises(KeyError): + self.store.get_missing(bad_state, 0, 6) + bad_state = {(0, 9999): set()} + with self.assertRaises(KeyError): + self.store.get_remaining(bad_state, 0, 9999) + def test_location_set_intersection(self) -> None: locations = {10, 11, 12} locations.intersection_update(self.store[1]) @@ -181,6 +212,16 @@ def test_no_locations(self) -> None: }) self.assertEqual(len(store), 1) self.assertEqual(len(store[1]), 0) + self.assertEqual(sorted(store.find_item(set(), 1)), []) + self.assertEqual(sorted(store.find_item({1}, 1)), []) + self.assertEqual(sorted(store.find_item({1, 2}, 1)), []) + self.assertEqual(store.get_for_player(1), {}) + self.assertEqual(store.get_checked(empty_state, 0, 1), []) + self.assertEqual(store.get_checked(full_state, 0, 1), []) + self.assertEqual(store.get_missing(empty_state, 0, 1), []) + self.assertEqual(store.get_missing(full_state, 0, 1), []) + self.assertEqual(store.get_remaining(empty_state, 0, 1), []) + self.assertEqual(store.get_remaining(full_state, 0, 1), []) def test_no_locations_for_1(self) -> None: store = self.type({ From 781100a571fe7e4850272a8899c5ef9d078f19d8 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:26:33 -0500 Subject: [PATCH 107/112] CI: remove version restriction on pytest-subtests (#4356) This reverts commit e3b5451672c694c12974801f5c89cc172db3ff5a. --- .github/workflows/unittests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 9db9de9b4042..88b5d12987ad 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -53,7 +53,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest "pytest-subtests<0.14.0" pytest-xdist + pip install pytest pytest-subtests pytest-xdist python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" python Launcher.py --update_settings # make sure host.yaml exists for tests - name: Unittests From 5dd19fccd0dc89966095c93c3fdb3ad72b4bea08 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:35:36 +0100 Subject: [PATCH 108/112] MultiServer/CommonClient: We forgot about Item Links again (Hint Priority) (#4314) * Vi don't forget about itemlinks challenge difficulty impossible * People other than Vi also don't forget about ItemLinks challenge difficulty impossible --- MultiServer.py | 2 +- NetUtils.py | 2 +- kvui.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 2561b0692a3c..0601e179152c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1914,7 +1914,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): hint = ctx.get_hint(client.team, player, location) if not hint: return # Ignored safely - if hint.receiving_player != client.slot: + if client.slot not in ctx.slot_set(hint.receiving_player): await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission', "original_cmd": cmd}]) diff --git a/NetUtils.py b/NetUtils.py index 196a030f4969..a961850639a0 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -232,7 +232,7 @@ def _handle_text(self, node: JSONMessagePart): def _handle_player_id(self, node: JSONMessagePart): player = int(node["text"]) - node["color"] = 'magenta' if player == self.ctx.slot else 'yellow' + node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow' node["text"] = self.ctx.player_names[player] return self._handle_color(node) diff --git a/kvui.py b/kvui.py index d98fc7ed9ab8..b2ab004e274a 100644 --- a/kvui.py +++ b/kvui.py @@ -371,7 +371,7 @@ def on_touch_down(self, touch): if self.hint["status"] == HintStatus.HINT_FOUND: return ctx = App.get_running_app().ctx - if ctx.slot == self.hint["receiving_player"]: # If this player owns this hint + if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint # open a dropdown self.dropdown.open(self.ids["status"]) elif self.selected: @@ -800,7 +800,7 @@ def refresh_hints(self, hints): hint_status_node = self.parser.handle_node({"type": "color", "color": status_colors.get(hint["status"], "red"), "text": status_names.get(hint["status"], "Unknown")}) - if hint["status"] != HintStatus.HINT_FOUND and hint["receiving_player"] == ctx.slot: + if hint["status"] != HintStatus.HINT_FOUND and ctx.slot_concerns_self(hint["receiving_player"]): hint_status_node = f"[u]{hint_status_node}[/u]" data.append({ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, From 925fb967d3274e64a8f88b73d81b97ce51c6d0d2 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 10 Dec 2024 14:36:38 -0500 Subject: [PATCH 109/112] Lingo: Fix number hunt issues on panels mode (#4342) --- worlds/lingo/data/generated.dat | Bin 149230 -> 149485 bytes worlds/lingo/test/TestDatafile.py | 7 ++++++- worlds/lingo/utils/pickle_static_data.py | 16 ++++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 9abb0276c8b5ac2fb1e81eff1e2c2cc06489ddec..8b159d4ae4ac7c565e2372b1cc43d7a349b2ca7c 100644 GIT binary patch delta 5053 zcma)ZV~ zz$q=awm#WI(@LE!b3fMlM#V=7mV~xjuDu_tpWUAOJLfkby0xG64}ahJp7T5RchB#4 z@71r?`+m8>cbOCXK-uFD%Lk#q8C0}XK!DEfHL0WwzfFM<+%pLFb42aXo*(4lfkVs? zhGF9&hO95nHiMtbH5k70q~G4YTBk@_?Og7}mxsV^*y@^NfVB$f@oFTjfe2g>1v!-I zgImoIfNw{^FE9)DMMDnEa{U?&Qx$jk(Jaz&KD6(5?S_K~gUuKG@gT_k<* z{9N$E0)(udb>j9)C~#%XfQ??T*VQ%~)+w+7XWk2TcoDDM3%O#5uYgP)@1(1?0%APH zazqdDskxA$)8bjTiX58D4;GxM>~);+%Pqq~wO-dn4p;@HN^X>E{wnCBS2_o|tQ+B&C$wUw6UK;Pi4#VPVYd?=g=;vz z8Agd=akG%u@Qr5Ct|5syt_9L)T+{;TG``jXk0@*8i4f~@twHGLNwr%x!<{-ZSN{kj z`av`f+5tsA(JAhgeZ3ea;@%zNz|C+~z6v`AKn4yx2uWg?co2q(!Fo`f)eL-F{`&MF z#EM`255X|CcCd$smVudvAX{B3dF3I9Q5}+Z9)ehPtK^G^AVJ+H+5a$%Q1#*fiMjcQ zVT?Y4d6b8TJ=(GViNUz-Fa)c!WWu4tkfpvU+4l%cQrjfYIszr?Cz5v^fg<%s$#4`( zRaKl7QT(x^;vGaR* zw;YFo`X71%363)vA$a~cM5v(=T;=EEFj~!zWX@~_lUgPD(N@v!#gfkwuaq2e0>-Nw zCC@nl+1^hvk8*tb_U-uA2^g-vD1)Dipt@Uf@=3^14@h2mQuq^+-#sacbx8I(CH!!4 zltoKRPCe_Yp?KjGgm?#v$5iypQF3e``nSOVHJk+w zZxan&I+TN=MvNmMEC`$0AV{CsBcn+=`nRgMuMPav4IIGJB1wHw@)vF5kkG4Mtg6px z2vg|=A^48dFwjQNv)ryeWtEE!r4`lYDyy@Yo<)AI$ynbq^Fnj=%~AR;xTATM^DXr^ zN9nC1qx#~?YV%FO2Fuk(r?xFtWT<>n;vARTgRtwg*wRf#nuqEc@pSx2a?%+XrEZs8 zeFoCiy^^<|5!-%A@)yKsCC8r?d{A=LS(u=jhjGo_#4{!TbQTiTVe#yf&WZX{5}2#c zL5A8YdB-`Js9ux&(>W+qha_^&r1yk3N#61vWU7x#{)l++2+oOkUp%?jlbG*%U#yv4 zBjUcA--lxT3}$&MYWhPs_B{{5`u35OK{f_Af3c>%2isLjQIch}rGp_`Q;^gyM{L zD1c@7Y&!(aT$e5`x=j34q|lbzeNzl&4b_V(>S~?ZjhLs5jr^^cmyD&}8pD7NaRg-z zyrTmm{FqnJwSeYeZIqJeEw8cy9UTy9@(6&97;w ztGE1jST0weUpeo$qH@7{aoTThkth)8u7qI@vY*^Jd9qQ;@jZnxK8mkar4Yh@v358iH)7& zqv+<>dU96c9jZN3VyQg(xTO#|3fB9zTyIavDb|=mB~z#L*0R4|GxMhORKem);+@V7;fnIC zdzMX{-g_e7oVcmHX?fi@#o2t)?Yg{)1*Y=e`QFqFyI1kV;+vM`!w*LHkyCTB%gQHF zf==9v#-}btHo}IHmm#T}#c?y>M>$GA_deNh{bd-Nxj+jPXV=o9JxoJ^Rz!nY3l@VG zO}x%B+tSanUrQttO6E1qKtrYFj7$9(0>gZoTMIns4~F~-zwU`p8WRJqWYPm+)UJxL z(Br7;;Rtv=$fpnlHE5sV$wVc?_X~=t?pRu(H^;u@iQ3l`^AX0XeZ$76eMibyDD8Wq zK1Bb-StFG2zSl?u@ciqzYJ^gK@4tAG_5%e1xy+B!{7hy5n_t)%HIFQzf~Xu24J1;C zf{8qd1`&A?4JOiMVOf%rY*U31pHv?T7(~FT`AVZF6TzmhH2uj$vhkPZHZpzL+%C-k zGEr=TqzNVy%_fA6Q5#GuMkppZ6I+GPcVZ2ODAU~2*6QxNaGm1?z8>2RoRJu@FD$!^`4O*Hz zn2x_BDDamO+#J%=V{5#{m3aoCokq!wh8F|nDz=~^zCLSd|0 zo-_qyirARg*u5uLSnJJoOPqz1Sr=i^NF~iSS>~5eeklh^rI|wJPBvxIOeIssW|}n9 z$&|C1AwVb2pm>(pbsNW>e3`s4XOQk5Kl$6VR5b7;_hqsSrk6OmrX75~BNw{y;QWP=mHq zpg84?WX!@?wI()3Z5e)^sze4Zr-+4fSGXf9F(OS#AFzrdRa9>^(St;5usTf%3VMV@ zEs3>63wZiE+?=MwjkS}hr`UR;2BOD^77}eBT12#wXtAIMZIeKH;W%1GDF&O9e`zfg zUn=5O4c+l4Nj*f9Hxo7T070FhK!BPv(h|AW*M93rFntO3O3uMd5I6} z!>nGGYC9#ZWV1t>on%(A*(J^EWLC3z1255S?2!Q%1s>tRpQU+=%y2gQq}fkqEt|hc zbAZe`Ht$IDS2B;XImpJS9VTTL%HF@cW|6thkXKz_S5bYx*;%;$s7j4J;(k(}OjpK7 z9H;chMS6qQ$`!OzayTQ0_wa{wCCt_?e_aqmVKZOv18FXjX<^gJ#;A3XLZP%vL{AW1 zCVEm(gZ43}X@4iPnJ0cC%@s0R*!)A9&&WI_Oq5moT&gcgJ+NMHzDQzrm z29Kt-*4kE2hGy$CZLM8wy&&EYQ`AJQZP(h?*4k%#T(kB)=Y{dI7yg*{_x;X!&v(A} zeZOJ;_Pp5*mp?wJ?@*U-yp#fKm*IhkPh?O^Syp3hc($6JR|Y#B>9! zg-(~t09Sz0eQ-|_jN{5)UPZ;@FeVk^T|);$2tW-MCqt1KUPy*1P~*Cu47YrhvC%lQ z7WCNP0tVOMEa>Y4yKwYq*e8bXM~hof!`M0omWkqdd62K{(!0C!c6H^!SRL3frV#GP z)Qlqd6{KKG5eyZ>wIaxX6j#4uSgR8ix0ga0cZZ&3kRz#QS-kOjAqxr zA6fMFba+cf?&cWRz(Y7-8wBE|L~dY24I32ks0*vX4o&z&HH;HO z_B_bNt^Fa$wR#>zdg4n-&<9&gkcw~5fFS(V1Ver7x{89S1*J}WJ_4d}%tDC9&IplK zuoe1Z(?S@4KgEjswk(A0%Gw0nTqE@C8c0`i`68djb2TtgF(zQVS&mmtqDo4w=rpkm z1}M`KFoxzG9xMuN6)M!B-7%DadPDf`M7kR7 z<|w?@3gK!Ei!H}Qv)%(ZZ^$u7Qc?1(V=&ZjY&3al(BMdWJsfL}i=+1GG0>~k9Ko-T zL9%Mo^X!N=7^Lo!T-XK)e%qL7cB13vo=C^&y*hlc4MP2{y8|hX_7UOuSsTQtJ~3S7 zr#2X=rVM1xISxf?rR4VG;yro!I1Ewex<#J4Nb)BqMKJk<=w-P(7>t$^FvNQei!8^@ zKlH`lpMb&Yrh77e78&XeDbi0uzWT;J!RJnj;P0dmHyERSEZO^%*!I4$++pb{Nb##> zrsqs>#MJe}9j74NyFp6F$%B#DN#bT4&B$_;wT5EgY3QfkW`Rkk#X?sM;-DxI@8}a1 zf-6r$h@ZDx#*uXNZd397(-5fEZ~#w z8gs>~C^yq@eC^T)5{>R)uZr5`_ZOjeSWfh>FfBFJ-5*_WH(F3zV{W)Vy2KsD*V-Y> zZ>{?}$#Ly`2zIrLt*9FGqr|CM;JcR;G@oyz$ z!VgO2{1-oiT=g-@mx*sl)}Isa_t|vLnR-t2OkXzQxt!;q)Gv#f4++-y7GK4F=f!gD z&!h~p@wll#3BfUe;Eg5c!GIs0hY0+hJ~p0*{+Qb-s8dkC;BrfSjm2cO(kABDFRHgJ zv@JrvP6)^2g-STu>0^2)6vAxW*$E+4=4^43+2W%_p+jN!&Cpla>Xubo9&>7UW1cdW z`QKt*GB)hN7zSN{5LMQ|iJ$UK+s1vCfibN#43|Ay54|PB29BjFC zcS`P6h{LV~CD0wMClg?}$LzccvDoxE1jTcgT)P)_Y1QU~hsR2%-K$`EZA16+RN?h& zpbxlT#eDG}D)6%SYUhGgm~**j>1Y1mrRV7XUiz85^s?wo{OB@_O6D8Pa~HMReJ9jg z8}xY=Q?-*Cr(cuZjnBixF7Y2m&cGR65G8pQuILi~CfU!zz3!Y@c(qG>r?cn%iNlpY zg137i#V!rRpgcu&8?jgMy>;~d%W1PlzUEs+$~37RQS}DPJ`XEa#~q;1J&-OvFj>}CKnXk>*Y#& zjW3%r`CcvGMX#!Y$?hsR@fw6S@=|a`dFDMUil^T@QO7T8_s>B2xSY*L-mWVsE-dQV z4EElf?^Rk{dS6>U{D$m+nglj%psL z{Or?wV!Q|axuQW?mD=Y}F1;BB?T8qS9!FIVN1uHmK1L8^(~jctG-ZVEF^Z|(vEfE< zjgJx~Nm?fbg1O8E zX)clJ$L4c32Ca)!2$j1+6iRfJD2(VDQGcQ@h$4u-%*Dw=m7$G)5@w}S`!fafB4E}2 zBF)!iV%U5m&9`I*vbiD6-^uv1xhc&LWMbL;Lz;h*iDUC48-sR>RJ>5yzxojlBD_tM zK=e~ChGi(x}_6r4m!v#D>akGjTj45n9rO}Z|Wuvk&Xg;LU@Vntk7&Ku48BV5>O_nqx$V_1~QkonxQ`zK7Gn&ja zHe(v48cS+At9)q+$jo3fUYa5@Rcwl-DIxO+n+ejCl9|b-Oqxk#X0e&f#-LS_nk|&w ze=M#Vt{BEmAyX}kHkD`|(KMp@MAL~D2(oE21d5aX2pN+wR&6F5gEkw#8LkWroyg`XCh(w$ACePJ&%VCck z_TjA&N>t;1dF2v=v6=7mo;2^1*}~=nHU{kgDHKZkkZ3E>M?~8M*|d*2O*=^DMV|Pn nG>6D+XLDGZqhxjn6KmC4rD`Mf5+@y(rd None: "LL1.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'") self.assertEqual(ids_file_hash, HASHES["ids.yaml"], "ids.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'") + + def test_panel_doors_are_set(self) -> None: + # This panel is defined earlier in the file than the panel door, so we want to check that the panel door is + # correctly applied. + self.assertNotEqual(PANELS_BY_ROOM["Outside The Agreeable"]["FIVE (1)"].panel_door, None) diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py index cd5c4b41df4b..df82a12861a4 100644 --- a/worlds/lingo/utils/pickle_static_data.py +++ b/worlds/lingo/utils/pickle_static_data.py @@ -111,6 +111,16 @@ def load_static_data(ll1_path, ids_path): with open(ll1_path, "r") as file: config = Utils.parse_yaml(file) + # We have to process all panel doors first so that panels can see what panel doors they're in even if they're + # defined earlier in the file than the panel door. + for room_name, room_data in config.items(): + if "panel_doors" in room_data: + PANEL_DOORS_BY_ROOM[room_name] = dict() + + for panel_door_name, panel_door_data in room_data["panel_doors"].items(): + process_panel_door(room_name, panel_door_name, panel_door_data) + + # Process the rest of the room. for room_name, room_data in config.items(): process_room(room_name, room_data) @@ -515,12 +525,6 @@ def process_room(room_name, room_data): for source_room, doors in room_data["entrances"].items(): process_entrance(source_room, doors, room_obj) - if "panel_doors" in room_data: - PANEL_DOORS_BY_ROOM[room_name] = dict() - - for panel_door_name, panel_door_data in room_data["panel_doors"].items(): - process_panel_door(room_name, panel_door_name, panel_door_data) - if "panels" in room_data: PANELS_BY_ROOM[room_name] = dict() From 704f14ffcd80d32a4c0ffed4bdf4cf38324b0dc5 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:37:54 -0500 Subject: [PATCH 110/112] Core: Add toggles_as_bools to options.as_dict (#3770) * Add toggles_as_bools to options.as_dict * Update Options.py Co-authored-by: Doug Hoskisson * Add param to docstring * if -> elif --------- Co-authored-by: Doug Hoskisson --- Options.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index d3b2e6c1ba11..4e26a0d56c5c 100644 --- a/Options.py +++ b/Options.py @@ -754,7 +754,7 @@ def __init__(self, value: int) -> None: elif value > self.range_end and value not in self.special_range_names.values(): raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " + f"and is also not one of the supported named special values: {self.special_range_names}") - + # See docstring for key in self.special_range_names: if key != key.lower(): @@ -1180,7 +1180,7 @@ def __len__(self) -> int: class Accessibility(Choice): """ Set rules for reachability of your items/locations. - + **Full:** ensure everything can be reached and acquired. **Minimal:** ensure what is needed to reach your goal can be acquired. @@ -1198,7 +1198,7 @@ class Accessibility(Choice): class ItemsAccessibility(Accessibility): """ Set rules for reachability of your items/locations. - + **Full:** ensure everything can be reached and acquired. **Minimal:** ensure what is needed to reach your goal can be acquired. @@ -1249,12 +1249,16 @@ class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: + def as_dict(self, + *option_names: str, + casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake", + toggles_as_bools: bool = False) -> 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` + :param toggles_as_bools: whether toggle options should be output as bools instead of strings """ assert option_names, "options.as_dict() was used without any option names." option_results = {} @@ -1276,6 +1280,8 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, value = getattr(self, option_name).value if isinstance(value, set): value = sorted(value) + elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle): + value = bool(value) option_results[display_name] = value else: raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") From 54a0a5ac0002ff1edf858441891625600b69e812 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:06:06 +0100 Subject: [PATCH 111/112] The Witness: Put progression + useful on some items. (#4027) * proguseful * ruff * variable rename * variable rename * Better (?) comment * Better way to do this? I guess * sure * ruff * Eh, it's not worth it. Here's the much simpler version * don't need this now * Improve some classification checks while we're at it * Only proguseful obelisk keys if eps are individual --- worlds/witness/player_items.py | 48 +++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 831e614f21c4..d1b951fa8e15 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -7,6 +7,7 @@ from BaseClasses import Item, ItemClassification, MultiWorld from .data import static_items as static_witness_items +from .data import static_logic as static_witness_logic from .data.item_definition_classes import ( DoorItemDefinition, ItemCategory, @@ -53,9 +54,8 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, # Remove all progression items that aren't actually in the game. self.item_data = { name: data for (name, data) in self.item_data.items() - if data.classification not in - {ItemClassification.progression, ItemClassification.progression_skip_balancing} - or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME + if ItemClassification.progression not in data.classification + or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME } # Downgrade door items @@ -72,7 +72,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, # Add progression items to the mandatory item list. progression_dict = { name: data for (name, data) in self.item_data.items() - if data.classification in {ItemClassification.progression, ItemClassification.progression_skip_balancing} + if ItemClassification.progression in data.classification } for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): @@ -100,6 +100,46 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT), ItemClassification.progression, False) + # Determine which items should be progression + useful, if they exist in some capacity. + # Note: Some of these may need to be updated for the "independent symbols" PR. + self._proguseful_items = { + "Dots", "Stars", "Shapers", "Black/White Squares", + "Caves Shortcuts", "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", + "Boat", + } + + if self._world.options.shuffle_EPs == "individual": + self._proguseful_items |= { + "Town Obelisk Key", # Most checks + "Monastery Obelisk Key", # Most sphere 1 checks, and also super dense ("Jackpot" vibes)} + } + + if self._world.options.shuffle_discarded_panels: + # Discards only give a moderate amount of checks, but are very spread out and a lot of them are in sphere 1. + # Thus, you really want to have the discard-unlocking item as quickly as possible. + + if self._world.options.puzzle_randomization in ("none", "sigma_normal"): + self._proguseful_items.add("Triangles") + elif self._world.options.puzzle_randomization == "sigma_expert": + self._proguseful_items.add("Arrows") + # Discards require two symbols in Variety, so the "sphere 1 unlocking power" of Arrows is not there. + if self._world.options.puzzle_randomization == "sigma_expert": + self._proguseful_items.add("Triangles") + self._proguseful_items.add("Full Dots") + self._proguseful_items.add("Stars + Same Colored Symbol") + self._proguseful_items.discard("Stars") # Stars are not that useful on their own. + if self._world.options.puzzle_randomization == "umbra_variety": + self._proguseful_items.add("Triangles") + + # This needs to be improved when the improved independent&progressive symbols PR is merged + for item in list(self._proguseful_items): + self._proguseful_items.add(static_witness_logic.get_parent_progressive_item(item)) + + for item_name, item_data in self.item_data.items(): + if item_name in self._proguseful_items: + item_data.classification |= ItemClassification.useful + + def get_mandatory_items(self) -> Dict[str, int]: """ Returns the list of items that must be in the pool for the game to successfully generate. From 9a37a136a1ab02c561b704a144b6424eac5db416 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:13:45 +0100 Subject: [PATCH 112/112] The Witness: Add more panels to the "doors: panels" mode (#2916) * Add more panels that should be panels * Make it so the caves panel items don't exist in early caves * Remove unused import * oops * Remove Jungle to Monastery Garden from usefulification list * Add a basic test * ruff --------- Co-authored-by: Fabian Dill --- worlds/witness/data/WitnessItems.txt | 12 ++++++-- .../Door_Shuffle/Complex_Door_Panels.txt | 5 ++++ .../settings/Door_Shuffle/Simple_Panels.txt | 4 +-- worlds/witness/data/settings/Early_Caves.txt | 6 +++- .../data/settings/Early_Caves_Start.txt | 6 +++- worlds/witness/player_items.py | 7 +++-- worlds/witness/player_logic.py | 12 ++++++-- worlds/witness/test/test_door_shuffle.py | 28 ++++++++++++++++++- 8 files changed, 67 insertions(+), 13 deletions(-) diff --git a/worlds/witness/data/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt index 782fa9c3d226..57aee28e45b6 100644 --- a/worlds/witness/data/WitnessItems.txt +++ b/worlds/witness/data/WitnessItems.txt @@ -56,6 +56,7 @@ Doors: 1119 - Quarry Stoneworks Entry (Panel) - 0x01E5A,0x01E59 1120 - Quarry Stoneworks Ramp Controls (Panel) - 0x03678,0x03676 1122 - Quarry Stoneworks Lift Controls (Panel) - 0x03679,0x03675 +1123 - Quarry Stoneworks Stairs (Panel) - 0x03677 1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 1129 - Quarry Boathouse Hook Control (Panel) - 0x275FA @@ -84,6 +85,7 @@ Doors: 1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x17CBC 1208 - Treehouse Drawbridge (Panel) - 0x037FF 1175 - Jungle Popup Wall (Panel) - 0x17CAB +1178 - Jungle Monastery Garden Shortcut (Panel) - 0x17CAA 1180 - Bunker Entry (Panel) - 0x17C2E 1183 - Bunker Tinted Glass Door (Panel) - 0x0A099 1186 - Bunker Elevator Control (Panel) - 0x0A079 @@ -94,12 +96,15 @@ Doors: 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 1196 - Swamp Long Bridge (Panel) - 0x17E2B 1197 - Swamp Maze Controls (Panel) - 0x17C0A,0x17E07 +1199 - Swamp Laser Shortcut (Panel) - 0x17C05 1220 - Mountain Floor 1 Light Bridge (Panel) - 0x09E39 1225 - Mountain Floor 2 Light Bridge Near (Panel) - 0x09E86 1230 - Mountain Floor 2 Light Bridge Far (Panel) - 0x09ED8 1235 - Mountain Floor 2 Elevator Control (Panel) - 0x09EEB 1240 - Caves Entry (Panel) - 0x00FF8 1242 - Caves Elevator Controls (Panel) - 0x335AB,0x335AC,0x3369D +1243 - Caves Mountain Shortcut (Panel) - 0x021D7 +1244 - Caves Swamp Shortcut (Panel) - 0x17CF2 1245 - Challenge Entry (Panel) - 0x0A16E 1250 - Tunnels Entry (Panel) - 0x039B4 1255 - Tunnels Town Shortcut (Panel) - 0x09E85 @@ -250,19 +255,20 @@ Doors: 2101 - Outside Tutorial Outpost Panels - 0x0A171,0x04CA4 2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249,0x0A015,0x09FA0,0x09F86 2110 - Quarry Outside Panels - 0x17C09,0x09E57,0x17CC4 -2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675 +2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675,0x03677 2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA 2122 - Keep Hedge Maze Panels - 0x00139,0x019DC,0x019E7,0x01A0F 2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10 +2127 - Jungle Panels - 0x17CAB,0x17CAA 2130 - Town Church & RGB House Panels - 0x28998,0x28A0D,0x334D8 2135 - Town Maze Panels - 0x2896A,0x28A79 2137 - Town Dockside House Panels - 0x0A0C8,0x09F98 2140 - Windmill & Theater Panels - 0x17D02,0x00815,0x17F5F,0x17F89,0x0A168,0x33AB2 2145 - Treehouse Panels - 0x0A182,0x0288C,0x02886,0x2700B,0x17CBC,0x037FF 2150 - Bunker Panels - 0x34BC5,0x34BC6,0x0A079,0x0A099,0x17C2E -2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E +2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E,0x17C05 2160 - Mountain Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB -2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC +2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC,0x021D7,0x17CF2 2170 - Tunnels Panels - 0x09E85,0x039B4 2200 - Desert Obelisk Key - 0x0332B,0x03367,0x28B8A,0x037B6,0x037B2,0x000F7,0x3351D,0x0053C,0x00771,0x335C8,0x335C9,0x337F8,0x037BB,0x220E4,0x220E5,0x334B9,0x334BC,0x22106,0x0A14C,0x0A14D,0x00359 diff --git a/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt index 63d8a58d2676..6c3b328691f9 100644 --- a/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt +++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt @@ -9,6 +9,7 @@ Desert Flood Room Entry (Panel) Quarry Entry 1 (Panel) Quarry Entry 2 (Panel) Quarry Stoneworks Entry (Panel) +Quarry Stoneworks Stairs (Panel) Shadows Door Timer (Panel) Keep Hedge Maze 1 (Panel) Keep Hedge Maze 2 (Panel) @@ -28,11 +29,15 @@ Treehouse Third Door (Panel) Treehouse Laser House Door Timer (Panel) Treehouse Drawbridge (Panel) Jungle Popup Wall (Panel) +Jungle Monastery Garden Shortcut (Panel) Bunker Entry (Panel) Bunker Tinted Glass Door (Panel) Swamp Entry (Panel) Swamp Platform Shortcut (Panel) +Swamp Laser Shortcut (Panel) Caves Entry (Panel) +Caves Mountain Shortcut (Panel) +Caves Swamp Shortcut (Panel) Challenge Entry (Panel) Tunnels Entry (Panel) Tunnels Town Shortcut (Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt index 23501d20d3a7..f9b8b1b43ae7 100644 --- a/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt +++ b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt @@ -7,6 +7,7 @@ Quarry Stoneworks Panels Quarry Boathouse Panels Keep Hedge Maze Panels Monastery Panels +Jungle Panels Town Church & RGB House Panels Town Maze Panels Windmill & Theater Panels @@ -18,5 +19,4 @@ Mountain Panels Caves Panels Tunnels Panels Glass Factory Entry (Panel) -Shadows Door Timer (Panel) -Jungle Popup Wall (Panel) \ No newline at end of file +Shadows Door Timer (Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Early_Caves.txt b/worlds/witness/data/settings/Early_Caves.txt index 48c8056bc7b6..df1e7b114a47 100644 --- a/worlds/witness/data/settings/Early_Caves.txt +++ b/worlds/witness/data/settings/Early_Caves.txt @@ -3,4 +3,8 @@ Caves Shortcuts Remove Items: Caves Mountain Shortcut (Door) -Caves Swamp Shortcut (Door) \ No newline at end of file +Caves Swamp Shortcut (Door) + +Forbidden Doors: +0x021D7 (Caves Mountain Shortcut Panel) +0x17CF2 (Caves Swamp Shortcut Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Early_Caves_Start.txt b/worlds/witness/data/settings/Early_Caves_Start.txt index a16a6d02bb9f..bc79007fa54b 100644 --- a/worlds/witness/data/settings/Early_Caves_Start.txt +++ b/worlds/witness/data/settings/Early_Caves_Start.txt @@ -6,4 +6,8 @@ Caves Shortcuts Remove Items: Caves Mountain Shortcut (Door) -Caves Swamp Shortcut (Door) \ No newline at end of file +Caves Swamp Shortcut (Door) + +Forbidden Doors: +0x021D7 (Caves Mountain Shortcut Panel) +0x17CF2 (Caves Swamp Shortcut Panel) \ No newline at end of file diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index d1b951fa8e15..2fb987bb456a 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -227,10 +227,13 @@ def get_door_ids_in_pool(self) -> List[int]: Returns the total set of all door IDs that are controlled by items in the pool. """ output: List[int] = [] - for item_name, item_data in dict(self.item_data.items()).items(): + + for item_name, item_data in self.item_data.items(): if not isinstance(item_data.definition, DoorItemDefinition): continue - output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] + + output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes + if hex_string not in self._logic.FORBIDDEN_DOORS] return output diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 58f15532f58c..9e6c9597382b 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -82,6 +82,7 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1) self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {} self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} + self.FORBIDDEN_DOORS: Set[str] = set() self.STARTING_INVENTORY: Set[str] = set() @@ -192,8 +193,9 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: for subset in these_items: self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(subset) - # Handle door entities (door shuffle) - if entity_hex in self.DOOR_ITEMS_BY_ID: + # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. + # Also, remove any original power requirements this entity might have had. + if entity_hex in self.DOOR_ITEMS_BY_ID and entity_hex not in self.FORBIDDEN_DOORS: # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) @@ -329,6 +331,10 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: if entity_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[entity_hex]: self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name) + if adj_type == "Forbidden Doors": + entity_hex = line[:7] + self.FORBIDDEN_DOORS.add(entity_hex) + if adj_type == "Starting Inventory": self.STARTING_INVENTORY.add(line) @@ -704,7 +710,7 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: self.make_single_adjustment(current_adjustment_type, line) - for entity_id in self.COMPLETELY_DISABLED_ENTITIES: + for entity_id in self.COMPLETELY_DISABLED_ENTITIES | self.FORBIDDEN_DOORS: if entity_id in self.DOOR_ITEMS_BY_ID: del self.DOOR_ITEMS_BY_ID[entity_id] diff --git a/worlds/witness/test/test_door_shuffle.py b/worlds/witness/test/test_door_shuffle.py index 0e38c32d69e2..d593a84bdb8f 100644 --- a/worlds/witness/test/test_door_shuffle.py +++ b/worlds/witness/test/test_door_shuffle.py @@ -1,4 +1,4 @@ -from ..test import WitnessTestBase +from ..test import WitnessMultiworldTestBase, WitnessTestBase class TestIndividualDoors(WitnessTestBase): @@ -22,3 +22,29 @@ def test_swamp_laser_shortcut(self) -> None: ], only_check_listed=True, ) + + +class TestForbiddenDoors(WitnessMultiworldTestBase): + options_per_world = [ + { + "early_caves": "off", + }, + { + "early_caves": "add_to_pool", + }, + ] + + common_options = { + "shuffle_doors": "panels", + "shuffle_postgame": True, + } + + def test_forbidden_doors(self) -> None: + self.assertTrue( + self.get_items_by_name("Caves Mountain Shortcut (Panel)", 1), + "Caves Mountain Shortcut (Panel) should exist in panels shuffle, but it didn't." + ) + self.assertFalse( + self.get_items_by_name("Caves Mountain Shortcut (Panel)", 2), + "Caves Mountain Shortcut (Panel) should be removed when Early Caves is enabled, but it still exists." + )
    Id