Compare commits

...

22 Commits

Author SHA1 Message Date
NewSoupVi
b7bca84e79 Update Utils.py 2025-07-11 23:27:59 +02:00
NewSoupVi
f128f2036c Revert "Core: Take Counter back out of RestrictedUnpickler #5169"
This reverts commit 95e09c8e2a.
2025-07-11 23:16:55 +02:00
Zach “Phar” Parks
6af34b66fb Various: Remove Rogue Legacy and Clique (#5177)
* Various: Remove Rogue Legacy and Clique

* Remove Clique from setup.py and revert network diagram.md change.

* Try again.

* Update network diagram.md

---------

Co-authored-by: Zach “Phar” Parks <phar@pharware.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-07-11 19:34:46 +02:00
NewSoupVi
2974f7d11f Core: Replace Clique with V6 in unit tests (#5181)
* replace Clique with V6 in unit tests

* no hard mode in V6

* modify regex in copy_world to allow : str

* oops

* I see now

* work around all typing

* there actually needs to be something
2025-07-11 19:27:28 +02:00
Carter Hesterman
edc0c89753 CIV 6: Remove Erroneous Boost Prereqs for Computers Boost (#5134) 2025-07-10 09:10:56 -04:00
axe-y
b1ff55dd06 DLCQ: Fix/Refactor LFoD Start Inventory (#5176) 2025-07-10 08:33:52 -04:00
Remy Jette
f4b5422f66 Factorio: Fix link to world_gen documentation (#5171) 2025-07-07 22:57:55 +02:00
massimilianodelliubaldini
d4ebace99f [Jak and Daxter] Auto Detect Install Path after Game Launcher Update #5152 2025-07-07 19:15:37 +02:00
NewSoupVi
95e09c8e2a Core: Take Counter back out of RestrictedUnpickler #5169 2025-07-07 16:24:35 +02:00
Fabian Dill
4623d59206 Core: ensure slot_data and er_hint_info are only base data types (#5144)
---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-07-07 15:51:39 +02:00
Doug Hoskisson
e68b1ad428 CommonClient: fix extra panels added to main_area_container (#5151) 2025-07-06 19:22:02 +02:00
Ixrec
072e2ece15 Docs: 'get_prefill_items' -> 'get_pre_fill_items' (#5167) 2025-07-05 17:01:08 -04:00
agilbert1412
11130037fe Stardew Valley: Fixed luck level requirements for slot machines #5160
# Conflicts:
#	worlds/stardew_valley/data/craftable_data.py
2025-07-03 21:08:36 +02:00
Scipio Wright
ba66ef14cc Update world api.md (#5149) 2025-07-02 14:14:35 +02:00
Jérémie Bolduc
8aacc23882 SDV: Add "Desert Transportation" and "Island Transportation" Item Groups (#5143) 2025-06-28 11:36:09 -04:00
Jonathan Tan
03e5fd3dae TWW: Fix Swords in Swordless Mode (#5137)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-28 10:46:37 -04:00
Fly Hyping
da52598c08 Wargroove: Fix Communication Thread (#5125) 2025-06-27 19:42:35 -04:00
Jonathan Tan
52389731eb TWW: Update Preset S7 to S8 (#5138) 2025-06-27 18:46:00 -04:00
LiquidCat64
21864f6f95 CVCotM: Fix Advance Collection ROM (#5132) 2025-06-27 18:25:45 -04:00
DJ-lennart
00f8625280 Civilization VI: Updated setup and info pages (#5123)
* Update setup_en.md

Updated setup instructions for Civilization VI in Archipelago

* Update en_Civilization VI.md

Updated info page for Civilization VI in Archipelago

* Update setup_en.md
2025-06-21 16:31:12 +02:00
James White
c34e29c712 Pokemon RB: Client: Send bounce messages with current map ID (#5121) 2025-06-20 22:52:54 +02:00
palex00
e0ae3359f1 Pokémon RB: Use new link for a new tracker (#5122)
* Update setup_en.md

* Update setup_es.md
2025-06-20 20:55:49 +02:00
55 changed files with 308 additions and 1749 deletions

View File

@@ -98,7 +98,7 @@ jobs:
shell: bash
run: |
cd build/exe*
cp Players/Templates/Clique.yaml Players/
cp Players/Templates/VVVVVV.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store 7z
uses: actions/upload-artifact@v4
@@ -189,7 +189,7 @@ jobs:
shell: bash
run: |
cd build/exe*
cp Players/Templates/Clique.yaml Players/
cp Players/Templates/VVVVVV.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store AppImage
uses: actions/upload-artifact@v4

View File

@@ -12,6 +12,7 @@ import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \
parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
from NetUtils import convert_to_base_types
from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple
from settings import get_settings
@@ -334,6 +335,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
}
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
for key in ("slot_data", "er_hint_data"):
multidata[key] = convert_to_base_types(multidata[key])
multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:

View File

@@ -106,6 +106,27 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
return obj
_base_types = str | int | bool | float | None | tuple["_base_types", ...] | dict["_base_types", "base_types"]
def convert_to_base_types(obj: typing.Any) -> _base_types:
if isinstance(obj, (tuple, list, set, frozenset)):
return tuple(convert_to_base_types(o) for o in obj)
elif isinstance(obj, dict):
return {convert_to_base_types(key): convert_to_base_types(value) for key, value in obj.items()}
elif obj is None or type(obj) in (str, int, float, bool):
return obj
# unwrap simple types to their base, such as StrEnum
elif isinstance(obj, str):
return str(obj)
elif isinstance(obj, int):
return int(obj)
elif isinstance(obj, float):
return float(obj)
else:
raise Exception(f"Cannot handle {type(obj)}")
_encode = JSONEncoder(
ensure_ascii=False,
check_circular=False,

View File

@@ -14,7 +14,6 @@ Currently, the following games are supported:
* Super Metroid
* Secret of Evermore
* Final Fantasy
* Rogue Legacy
* VVVVVV
* Raft
* Super Mario 64
@@ -41,7 +40,6 @@ Currently, the following games are supported:
* The Messenger
* Kingdom Hearts 2
* The Legend of Zelda: Link's Awakening DX
* Clique
* Adventure
* DLC Quest
* Noita

View File

@@ -442,6 +442,7 @@ class RestrictedUnpickler(pickle.Unpickler):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by OptionCounter
# necessary because the actual Options class instances are pickled when transfered to WebHost generation pool
if module == "collections" and name == "Counter":
return collections.Counter
# used by MultiServer -> savegame/multidata

View File

@@ -48,9 +48,6 @@
# Civilization VI
/worlds/civ6/ @hesto2
# Clique
/worlds/clique/ @ThePhar
# Dark Souls III
/worlds/dark_souls_3/ @Marechal-L @nex3
@@ -148,9 +145,6 @@
# Raft
/worlds/raft/ @SunnyBat
# Rogue Legacy
/worlds/rogue_legacy/ @ThePhar
# Risk of Rain 2
/worlds/ror2/ @kindasneaki

View File

@@ -125,10 +125,8 @@ flowchart LR
NM[Mod with Archipelago.MultiClient.Net]
subgraph FNA/XNA
TS[Timespinner]
RL[Rogue Legacy]
end
NM <-- TsRandomizer --> TS
NM <-- RogueLegacyRandomizer --> RL
subgraph Unity
ROR[Risk of Rain 2]
SN[Subnautica]
@@ -177,4 +175,4 @@ flowchart LR
FMOD <--> FMAPI
end
CC <-- Integrated --> FC
```
```

View File

@@ -266,7 +266,7 @@ 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 (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
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#L310-L311)),
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
@@ -533,7 +533,7 @@ In addition, the following methods can be implemented and are called in this ord
called to modify item placement before, during, and after the regular fill process; all finishing before
`generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there
are any items that need to be filled this way, but need to be in state while you fill other items, they can be
returned from `get_prefill_items`.
returned from `get_pre_fill_items`.
* `generate_output(self, output_directory: str)`
creates the output files if there is output to be generated. When this is called,
`self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the

View File

@@ -921,9 +921,11 @@ class GameManager(ThemedApp):
hint_panel = self.add_client_tab("Hints", HintLayout(self.hint_log))
self.log_panels["Hints"] = hint_panel.content
self.main_area_container = MDGridLayout(size_hint_y=1, cols=1)
self.main_area_container.add_widget(self.tabs)
self.main_area_container.add_widget(self.screens)
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
tab_container = MDGridLayout(size_hint_y=1, cols=1)
tab_container.add_widget(self.tabs)
tab_container.add_widget(self.screens)
self.main_area_container.add_widget(tab_container)
self.grid.add_widget(self.main_area_container)

View File

@@ -63,7 +63,6 @@ non_apworlds: set[str] = {
"Adventure",
"ArchipIDLE",
"Archipelago",
"Clique",
"Lufia II Ancient Cave",
"Meritous",
"Ocarina of Time",

View File

@@ -1,7 +1,7 @@
import unittest
from Fill import distribute_items_restrictive
from NetUtils import encode
from NetUtils import convert_to_base_types
from worlds.AutoWorld import AutoWorldRegister, call_all
from worlds import failed_world_loads
from . import setup_solo_multiworld
@@ -47,7 +47,7 @@ class TestImplemented(unittest.TestCase):
call_all(multiworld, "post_fill")
for key, data in multiworld.worlds[1].fill_slot_data().items():
self.assertIsInstance(key, str, "keys in slot data must be a string")
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
convert_to_base_types(data) # only put base data types into slot data
def test_no_failed_world_loads(self):
if failed_world_loads:

View File

@@ -63,12 +63,12 @@ if __name__ == "__main__":
spacer = '=' * 80
with TemporaryDirectory() as tempdir:
multis = [["Clique"], ["Temp World"], ["Clique", "Temp World"]]
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
p1_games = []
data_paths = []
rooms = []
copy_world("Clique", "Temp World")
copy_world("VVVVVV", "Temp World")
try:
for n, games in enumerate(multis, 1):
print(f"Generating [{n}] {', '.join(games)}")
@@ -101,7 +101,7 @@ if __name__ == "__main__":
with Client(host.address, game, "Player1") as client:
local_data_packages = client.games_packages
local_collected_items = len(client.checked_locations)
if collected_items < 2: # Clique only has 2 Locations
if collected_items < 2: # Don't collect anything on the last iteration
client.collect_any()
# TODO: Ctrl+C test here as well
@@ -125,7 +125,7 @@ if __name__ == "__main__":
with Client(host.address, game, "Player1") as client:
web_data_packages = client.games_packages
web_collected_items = len(client.checked_locations)
if collected_items < 2: # Clique only has 2 Locations
if collected_items < 2: # Don't collect anything on the last iteration
client.collect_any()
if collected_items == 1:
sleep(1) # wait for the server to collect the item

View File

@@ -34,7 +34,7 @@ def _generate_local_inner(games: Iterable[str],
f.write(json.dumps({
"name": f"Player{n}",
"game": game,
game: {"hard_mode": "true"},
game: {},
"description": f"generate_local slot {n} ('Player{n}'): {game}",
}))

View File

@@ -30,7 +30,7 @@ def copy(src: str, dst: str) -> None:
_new_worlds[dst] = str(dst_folder)
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
contents = f.read()
contents = re.sub(r'game\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
f.write(contents)

View File

@@ -382,7 +382,7 @@ class World(metaclass=AutoWorldRegister):
def create_items(self) -> None:
"""
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_pre_fill_items`.
"""
pass

View File

@@ -78,8 +78,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_IRON_WORKING",
"ERA_CLASSICAL",
["TECH_MINING"],
1,
["TECH_MINING", "TECH_BRONZE_WORKING"],
2,
"DEFAULT",
),
CivVIBoostData(
@@ -165,15 +165,9 @@ boosts: List[CivVIBoostData] = [
"BOOST_TECH_CASTLES",
"ERA_MEDIEVAL",
[
"CIVIC_DIVINE_RIGHT",
"CIVIC_EXPLORATION",
"CIVIC_REFORMED_CHURCH",
"CIVIC_SUFFRAGE",
"CIVIC_TOTALITARIANISM",
"CIVIC_CLASS_STRUGGLE",
"CIVIC_DIGITAL_DEMOCRACY",
"CIVIC_CORPORATE_LIBERTARIANISM",
"CIVIC_SYNTHETIC_TECHNOCRACY",
],
1,
"DEFAULT",
@@ -393,9 +387,6 @@ boosts: List[CivVIBoostData] = [
"CIVIC_SUFFRAGE",
"CIVIC_TOTALITARIANISM",
"CIVIC_CLASS_STRUGGLE",
"CIVIC_DIGITAL_DEMOCRACY",
"CIVIC_CORPORATE_LIBERTARIANISM",
"CIVIC_SYNTHETIC_TECHNOCRACY",
],
1,
"DEFAULT",

View File

@@ -20,16 +20,17 @@ A short period after receiving an item, you will get a notification indicating y
## FAQs
- Do I need the DLC to play this?
- Yes, you need both Rise & Fall and Gathering Storm.
- You need both expansions, Rise & Fall and Gathering Storm. You do not need the other DLCs but they fully work with this.
- Does this work with Multiplayer?
- It does not and, despite my best efforts, probably won't until there's a new way for external programs to be able to interact with the game.
- Does my mod that reskins Barbarians as various Pro Wrestlers work with this?
- Only one way to find out! Any mods that modify techs/civics will most likely cause issues, though.
- Does this work with other mods?
- A lot of mods seem to work without issues combined with this, but you should avoid any mods that change things in the tech or civic tree, as even if they would work it could cause issues with the logic.
- "Help! I can't see any of the items that have been sent to me!"
- Both trees by default will show you the researchable Archipelago locations. To view the normal tree, you can click "Toggle Archipelago Tree" in the top-left corner of the tree view.
- "Oh no! I received the Machinery tech and now instead of getting an Archer next turn, I have to wait an additional 10 turns to get a Crossbowman!"
- Vanilla prevents you from building units of the same class from an earlier tech level after you have researched a later variant. For example, this could be problematic if someone unlocks Crossbowmen for you right out the gate since you won't be able to make Archers (which have a much lower production cost).
Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
- Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
- If you think you should be able to make Field Cannons but seemingly can't try disabling `Telecommunications`
- "How does DeathLink work? Am I going to have to start a new game every time one of my friends dies?"
- Heavens no, my fellow Archipelago appreciator. When configuring your Archipelago options for Civilization on the options page, there are several choices available for you to fine tune the way you'd like to be punished for the follies of your friends. These include: Having a random unit destroyed, losing a percentage of gold or faith, or even losing a point on your era score. If you can't make up your mind, you can elect to have any of them be selected every time a death link is sent your way.
In the event you lose one of your units in combat (this means captured units don't count), then you will send a death link event to the rest of your friends.
@@ -39,7 +40,8 @@ Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to
1. `TECH_WRITING`
2. `TECH_EDUCATION`
3. `TECH_CHEMISTRY`
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.json).
- An important thing to note is that the seaport is part of progressive industrial zones, due to electricity having both an industrial zone building and the seaport.
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.py).
## Boostsanity
Boostsanity takes all of the Eureka & Inspiration events and makes them location checks. This feature is the one to change up the way Civilization is played in an AP multiworld/randomizer. What normally are mundane tasks that are passively collected now become a novel and interesting bucket list that you need to pay attention to in order to unlock items for yourself and others!
@@ -56,4 +58,3 @@ Boosts have logic associated with them in order to verify you can always reach t
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
- There's too many boosts, how will I know which one's I should focus on?!
- In order to give a little more focus to all the boosts rather than just arbitrarily picking them at random, items in both of the vanilla trees will now have an advisor icon on them if its associated boost contains a progression item.

View File

@@ -6,12 +6,14 @@ This guide is meant to help you get up and running with Civilization VI in Archi
The following are required in order to play Civ VI in Archipelago:
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux)
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux).
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) v0.4.5 or higher.
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- The latest version of the [Civ VI AP Mod](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
## Enabling the tuner
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
@@ -20,27 +22,32 @@ In the main menu, navigate to the "Game Options" page. On the "Game" menu, make
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
5. Your finished mod folder should look something like this:
- Civ VI Mods Directory
- civilization_archipelago_mod
- NewItems.xml
- InitOptions.lua
- Archipelago.modinfo
- All the other mod files, etc.
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
## Configuring your game
When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
## Troubleshooting
- If you have troubles with file extension related stuff, make sure Windows shows file extensions as they are turned off by default. If you don't know how to turn them on it is just a quick google search away.
- If you are getting an error: "The remote computer refused the network connection", or something else related to the client (or tuner) not being able to connect, it likely indicates the tuner is not actually enabled. One simple way to verify that it is enabled is, after completing the setup steps, go to Main Menu &rarr; Options &rarr; Look for an option named "Tuner" and verify it is set to "Enabled"
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are.
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are. This can resend certain items to you, like one time bonuses.
- If the archipelago mod does not appear in the mod selector in the game, make sure the mod is correctly placed as a folder in the `Sid Meier's Civilization VI\Mods` folder, there should not be any loose files in there only folders. As in the path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
- If it still does not appear make sure you have the right folder, one way to verify you are in the right place is to find the general folder area where your Civ VI save files are located.
- If you get an error when trying to start a game saying `Error - One or more Mods failed to load content`, make sure the files from the `.apcivvi` are placed into the `civilization_archipelago_mod` as loose files and not as a folder.
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.

View File

@@ -1,38 +0,0 @@
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING:
from . import CliqueWorld
class CliqueItem(Item):
game = "Clique"
class CliqueItemData(NamedTuple):
code: Optional[int] = None
type: ItemClassification = ItemClassification.filler
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
item_data_table: Dict[str, CliqueItemData] = {
"Feeling of Satisfaction": CliqueItemData(
code=69696969,
type=ItemClassification.progression,
),
"Button Activation": CliqueItemData(
code=69696968,
type=ItemClassification.progression,
can_create=lambda world: world.options.hard_mode,
),
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
code=69696967,
can_create=lambda world: False # Only created from `get_filler_item_name`.
),
"The Urge to Push": CliqueItemData(
type=ItemClassification.progression,
),
}
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}

View File

@@ -1,37 +0,0 @@
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import Location
if TYPE_CHECKING:
from . import CliqueWorld
class CliqueLocation(Location):
game = "Clique"
class CliqueLocationData(NamedTuple):
region: str
address: Optional[int] = None
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
locked_item: Optional[str] = None
location_data_table: Dict[str, CliqueLocationData] = {
"The Big Red Button": CliqueLocationData(
region="The Button Realm",
address=69696969,
),
"The Item on the Desk": CliqueLocationData(
region="The Button Realm",
address=69696968,
can_create=lambda world: world.options.hard_mode,
),
"In the Player's Mind": CliqueLocationData(
region="The Button Realm",
locked_item="The Urge to Push",
),
}
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
locked_locations = {name: data for name, data in location_data_table.items() if data.locked_item}

View File

@@ -1,34 +0,0 @@
from dataclasses import dataclass
from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool
class HardMode(Toggle):
"""Only for the most masochistically inclined... Requires button activation!"""
display_name = "Hard Mode"
class ButtonColor(Choice):
"""Customize your button! Now available in 12 unique colors."""
display_name = "Button Color"
option_red = 0
option_orange = 1
option_yellow = 2
option_green = 3
option_cyan = 4
option_blue = 5
option_magenta = 6
option_purple = 7
option_pink = 8
option_brown = 9
option_white = 10
option_black = 11
@dataclass
class CliqueOptions(PerGameCommonOptions):
color: ButtonColor
hard_mode: HardMode
start_inventory_from_pool: StartInventoryPool
# DeathLink is always on. Always.
# death_link: DeathLink

View File

@@ -1,11 +0,0 @@
from typing import Dict, List, NamedTuple
class CliqueRegionData(NamedTuple):
connecting_regions: List[str] = []
region_data_table: Dict[str, CliqueRegionData] = {
"Menu": CliqueRegionData(["The Button Realm"]),
"The Button Realm": CliqueRegionData(),
}

View File

@@ -1,13 +0,0 @@
from typing import Callable, TYPE_CHECKING
from BaseClasses import CollectionState
if TYPE_CHECKING:
from . import CliqueWorld
def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]:
if world.options.hard_mode:
return lambda state: state.has("Button Activation", world.player)
return lambda state: True

View File

@@ -1,102 +0,0 @@
from typing import List, Dict, Any
from BaseClasses import Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import CliqueItem, item_data_table, item_table
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
from .Options import CliqueOptions
from .Regions import region_data_table
from .Rules import get_button_rule
class CliqueWebWorld(WebWorld):
theme = "partyTime"
setup_en = Tutorial(
tutorial_name="Start Guide",
description="A guide to playing Clique.",
language="English",
file_name="guide_en.md",
link="guide/en",
authors=["Phar"]
)
setup_de = Tutorial(
tutorial_name="Anleitung zum Anfangen",
description="Eine Anleitung um Clique zu spielen.",
language="Deutsch",
file_name="guide_de.md",
link="guide/de",
authors=["Held_der_Zeit"]
)
tutorials = [setup_en, setup_de]
game_info_languages = ["en", "de"]
class CliqueWorld(World):
"""The greatest game of all time."""
game = "Clique"
web = CliqueWebWorld()
options: CliqueOptions
options_dataclass = CliqueOptions
location_name_to_id = location_table
item_name_to_id = item_table
def create_item(self, name: str) -> CliqueItem:
return CliqueItem(name, item_data_table[name].type, item_data_table[name].code, self.player)
def create_items(self) -> None:
item_pool: List[CliqueItem] = []
for name, item in item_data_table.items():
if item.code and item.can_create(self):
item_pool.append(self.create_item(name))
self.multiworld.itempool += item_pool
def create_regions(self) -> None:
# Create regions.
for region_name in region_data_table.keys():
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)
# Create locations.
for region_name, region_data in region_data_table.items():
region = self.get_region(region_name)
region.add_locations({
location_name: location_data.address for location_name, location_data in location_data_table.items()
if location_data.region == region_name and location_data.can_create(self)
}, CliqueLocation)
region.add_exits(region_data_table[region_name].connecting_regions)
# Place locked locations.
for location_name, location_data in locked_locations.items():
# Ignore locations we never created.
if not location_data.can_create(self):
continue
locked_item = self.create_item(location_data_table[location_name].locked_item)
self.get_location(location_name).place_locked_item(locked_item)
# Set priority location for the Big Red Button!
self.options.priority_locations.value.add("The Big Red Button")
def get_filler_item_name(self) -> str:
return "A Cool Filler Item (No Satisfaction Guaranteed)"
def set_rules(self) -> None:
button_rule = get_button_rule(self)
self.get_location("The Big Red Button").access_rule = button_rule
self.get_location("In the Player's Mind").access_rule = button_rule
# Do not allow button activations on buttons.
self.get_location("The Big Red Button").item_rule = lambda item: item.name != "Button Activation"
# Completion condition.
self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player)
def fill_slot_data(self) -> Dict[str, Any]:
return {
"color": self.options.color.current_key
}

View File

@@ -1,18 +0,0 @@
# Clique
## Was ist das für ein Spiel?
~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~
~~(rote) Knöpfe zu drücken.~~
Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach
es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten
Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand
anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann.
Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden.
## Wo ist die Seite für die Einstellungen?
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
eine YAML-Datei zu konfigurieren und zu exportieren.

View File

@@ -1,16 +0,0 @@
# Clique
## What is this game?
~~Clique is a psychological survival horror game where a player must survive the temptation to press red buttons.~~
Clique is a joke game developed for Archipelago in March 2023 to showcase how easy it can be to develop a world for
Archipelago. The objective of the game is to press the big red button. If a player is playing on `hard_mode`, they must
wait for someone else in the multiworld to "activate" their button before they can press it.
Clique can be played on most modern HTML5-capable browsers.
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure
and export a config file.

View File

@@ -1,25 +0,0 @@
# Clique Anleitung
Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib
Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden).
Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten.
Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst.
Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf
deinem Handy starten und produktiv sein während du wartest!
Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche
(mindestens) eins der Folgenden:
- Dein Zimmer aufräumen.
- Die Wäsche machen.
- Etwas Essen von einem X-Belieben Fast Food Restaruant holen.
- Das tägliche Wordle machen.
- ~~Deine Seele an **Phar** verkaufen.~~
- Deine Hausaufgaben erledigen.
- Deine Post abholen.
~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~
~~Discord kontaktieren. *zwinker* *zwinker*~~

View File

@@ -1,22 +0,0 @@
# Clique Start Guide
After rolling your seed, go to the [Clique Game](http://clique.pharware.com/) site and enter the server details, your
slot name, and a room password if one is required. Then click "Connect".
If you're playing on "easy mode", just click the button and receive "Satisfaction".
If you're playing on "hard mode", you may need to wait for activation before you can complete your objective. Luckily,
Clique runs in most major browsers that support HTML5, so you can load Clique on your phone and be productive while
you wait!
If you need some ideas for what to do while waiting for button activation, give the following a try:
- Clean your room.
- Wash the dishes.
- Get some food from a non-descript fast food restaurant.
- Do the daily Wordle.
- ~~Sell your soul to Phar.~~
- Do your school work.
~~If you run into any issues with this game, definitely do not contact **thephar** on discord. *wink* *wink*~~

View File

@@ -734,8 +734,8 @@ def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bo
magic_items_array[array_offset] += 1
# Add the start inventory arrays to the offset data in bytes form.
start_inventory_data[0x680080] = bytes(magic_items_array)
start_inventory_data[0x6800A0] = bytes(cards_array)
start_inventory_data[0x690080] = bytes(magic_items_array)
start_inventory_data[0x6900A0] = bytes(cards_array)
# Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max
# possible Max Ups.

View File

@@ -132,40 +132,40 @@ start_inventory_giver = [
# Magic Items
0x13, 0x48, # ldr r0, =0x202572F
0x14, 0x49, # ldr r1, =0x8680080
0x14, 0x49, # ldr r1, =0x8690080
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x08, 0x2A, # cmp r2, #8
0xFA, 0xDB, # blt 0x8680006
0xFA, 0xDB, # blt 0x8690006
# Max Ups
0x11, 0x48, # ldr r0, =0x202572C
0x12, 0x49, # ldr r1, =0x8680090
0x12, 0x49, # ldr r1, =0x8690090
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x03, 0x2A, # cmp r2, #3
0xFA, 0xDB, # blt 0x8680016
0xFA, 0xDB, # blt 0x8690016
# Cards
0x0F, 0x48, # ldr r0, =0x2025674
0x10, 0x49, # ldr r1, =0x86800A0
0x10, 0x49, # ldr r1, =0x86900A0
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x14, 0x2A, # cmp r2, #0x14
0xFA, 0xDB, # blt 0x8680026
0xFA, 0xDB, # blt 0x8690026
# Inventory Items (not currently supported)
0x0D, 0x48, # ldr r0, =0x20256ED
0x0E, 0x49, # ldr r1, =0x86800C0
0x0E, 0x49, # ldr r1, =0x86900C0
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x36, 0x2A, # cmp r2, #36
0xFA, 0xDB, # blt 0x8680036
0xFA, 0xDB, # blt 0x8690036
# Return to the function that checks for Magician Mode.
0xBA, 0x21, # movs r1, #0xBA
0x89, 0x00, # lsls r1, r1, #2
@@ -176,13 +176,13 @@ start_inventory_giver = [
# LDR number pool
0x78, 0x7F, 0x00, 0x08,
0x2F, 0x57, 0x02, 0x02,
0x80, 0x00, 0x68, 0x08,
0x80, 0x00, 0x69, 0x08,
0x2C, 0x57, 0x02, 0x02,
0x90, 0x00, 0x68, 0x08,
0x90, 0x00, 0x69, 0x08,
0x74, 0x56, 0x02, 0x02,
0xA0, 0x00, 0x68, 0x08,
0xA0, 0x00, 0x69, 0x08,
0xED, 0x56, 0x02, 0x02,
0xC0, 0x00, 0x68, 0x08,
0xC0, 0x00, 0x69, 0x08,
]
max_max_up_checker = [

View File

@@ -335,8 +335,8 @@ class CVCotMPatchExtensions(APPatchExtension):
rom_data.write_bytes(0x679A60, patches.kickless_roc_height_shortener)
# Give the player their Start Inventory upon entering their name on a new file.
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x68, 0x08])
rom_data.write_bytes(0x680000, patches.start_inventory_giver)
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x69, 0x08])
rom_data.write_bytes(0x690000, patches.start_inventory_giver)
# Prevent Max Ups from exceeding 255.
rom_data.write_bytes(0x5E170, [0x00, 0x4A, 0x97, 0x46, 0x00, 0x00, 0x6A, 0x08])

View File

@@ -30,7 +30,6 @@ class Group(enum.Enum):
Deprecated = enum.auto()
@dataclass(frozen=True)
class ItemData:
code_without_offset: offset
@@ -98,14 +97,15 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed
return traps
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random):
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str],
random: Random):
created_items = []
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
create_items_basic(world_options, created_items, world, excluded_items)
create_items_campaign(world_options, created_items, world, excluded_items, Group.DLCQuest, 825, 250)
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
world_options.campaign == Options.Campaign.option_both):
create_items_lfod(world_options, created_items, world, excluded_items)
create_items_campaign(world_options, created_items, world, excluded_items, Group.Freemium, 889, 200)
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
created_items += trap_items
@@ -113,27 +113,8 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count:
return created_items
def create_items_lfod(world_options, created_items, world, excluded_items):
for item in items_by_group[Group.Freemium]:
if item.name in excluded_items:
excluded_items.remove(item)
continue
if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
created_items.append(world.create_item(item))
if item.has_any_group(Group.Twice):
created_items.append(world.create_item(item))
if world_options.coinsanity == Options.CoinSanity.option_coin:
if world_options.coinbundlequantity == -1:
create_coin_piece(created_items, world, 889, 200, Group.Freemium)
return
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
def create_items_basic(world_options, created_items, world, excluded_items):
for item in items_by_group[Group.DLCQuest]:
def create_items_campaign(world_options: Options.DLCQuestOptions, created_items: list[DLCQuestItem], world, excluded_items: list[str], group: Group, total_coins: int, required_coins: int):
for item in items_by_group[group]:
if item.name in excluded_items:
excluded_items.remove(item.name)
continue
@@ -146,14 +127,15 @@ def create_items_basic(world_options, created_items, world, excluded_items):
created_items.append(world.create_item(item))
if world_options.coinsanity == Options.CoinSanity.option_coin:
if world_options.coinbundlequantity == -1:
create_coin_piece(created_items, world, 825, 250, Group.DLCQuest)
create_coin_piece(created_items, world, total_coins, required_coins, group)
return
create_coin(world_options, created_items, world, 825, 250, Group.DLCQuest)
create_coin(world_options, created_items, world, total_coins, required_coins, group)
def create_coin(world_options, created_items, world, total_coins, required_coins, group):
coin_bundle_required = math.ceil(required_coins / world_options.coinbundlequantity)
coin_bundle_useful = math.ceil((total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
coin_bundle_useful = math.ceil(
(total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
for item in items_by_group[group]:
if item.has_any_group(Group.Coin):
for i in range(coin_bundle_required):
@@ -165,7 +147,7 @@ def create_coin(world_options, created_items, world, total_coins, required_coins
def create_coin_piece(created_items, world, total_coins, required_coins, group):
for item in items_by_group[group]:
if item.has_any_group(Group.Piece):
for i in range(required_coins*10):
for i in range(required_coins * 10):
created_items.append(world.create_item(item))
for i in range((total_coins - required_coins) * 10):
created_items.append(world.create_item(item, ItemClassification.useful))

View File

@@ -321,7 +321,7 @@ class InventorySpillTrapCount(TrapCount):
class FactorioWorldGen(OptionDict):
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
with in-depth documentation at https://lua-api.factorio.com/latest/concepts/MapGenSettings.html"""
display_name = "World Generation"
# FIXME: do we want default be a rando-optimized default or in-game DS?
value: dict[str, dict[str, typing.Any]]

View File

@@ -367,7 +367,7 @@ def find_root_directory(ctx: JakAndDaxterContext):
f" Close all launchers, games, clients, and console windows, then restart Archipelago.")
if not os.path.exists(settings_path):
msg = (f"{err_title}: the OpenGOAL settings file does not exist.\n"
msg = (f"{err_title}: The OpenGOAL settings file does not exist.\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
@@ -375,14 +375,44 @@ def find_root_directory(ctx: JakAndDaxterContext):
with open(settings_path, "r") as f:
load = json.load(f)
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
# This settings file has changed format once before, and may do so again in the future.
# Guard against future incompatibilities by checking the file version first, and use that to determine
# what JSON keys to look for next.
try:
settings_version = load["version"]
logger.debug(f"OpenGOAL settings file version: {settings_version}")
except KeyError:
msg = (f"{err_title}: The OpenGOAL settings file has no version number!\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
try:
if settings_version == "2.0":
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
elif settings_version == "3.0":
jak1_installed = load["games"]["jak1"]["isInstalled"]
mod_sources = load["games"]["jak1"]["mods"]
else:
msg = (f"{err_title}: The OpenGOAL settings file has an unknown version number ({settings_version}).\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
except KeyError as e:
msg = (f"{err_title}: The OpenGOAL settings file does not contain key entry {e}!\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
if not jak1_installed:
msg = (f"{err_title}: The OpenGOAL Launcher is missing a normal install of Jak 1!\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
if mod_sources is None:
msg = (f"{err_title}: No mod sources have been configured in the OpenGOAL Launcher!\n"
f"{alt_instructions}")

View File

@@ -23,6 +23,7 @@ DATA_LOCATIONS = {
"DexSanityFlag": (0x1A71, 19),
"GameStatus": (0x1A84, 0x01),
"Money": (0x141F, 3),
"CurrentMap": (0x1436, 1),
"ResetCheck": (0x0100, 4),
# First and second Vermilion Gym trash can selection. Second is not used, so should always be 0.
# First should never be above 0x0F. This is just before Event Flags.
@@ -65,6 +66,7 @@ class PokemonRBClient(BizHawkClient):
self.banking_command = None
self.game_state = False
self.last_death_link = 0
self.current_map = 0
async def validate_rom(self, ctx):
game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")])
@@ -230,6 +232,10 @@ class PokemonRBClient(BizHawkClient):
}])
self.banking_command = None
if data["CurrentMap"][0] != self.current_map:
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot], "data": {"currentMap": data["CurrentMap"][0]}}])
self.current_map = data["CurrentMap"][0]
# VICTORY
if data["EventFlag"][280] & 1 and not ctx.finished_game:

View File

@@ -15,7 +15,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Optional Software
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Configuring BizHawk
@@ -109,7 +109,7 @@ server uses password, type in the bottom textfield `/connect <address>:<port> [p
Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking.
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Open PopTracker, and load the Pokémon Red and Blue pack.
3. Click on the "AP" symbol at the top.
4. Enter the AP address, slot name and password.

View File

@@ -16,7 +16,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
## Software Opcional
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Configurando BizHawk
@@ -114,7 +114,7 @@ presiona enter (si el servidor usa contraseña, escribe en el campo de texto inf
Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático.
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Abre PopTracker, y carga el pack de Pokémon Red and Blue.
3. Haz clic en el símbolo "AP" en la parte superior.
4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay).

View File

@@ -1,111 +0,0 @@
from typing import Dict, NamedTuple, Optional
from BaseClasses import Item, ItemClassification
class RLItem(Item):
game: str = "Rogue Legacy"
class RLItemData(NamedTuple):
category: str
code: Optional[int] = None
classification: ItemClassification = ItemClassification.filler
max_quantity: int = 1
weight: int = 1
def get_items_by_category(category: str) -> Dict[str, RLItemData]:
item_dict: Dict[str, RLItemData] = {}
for name, data in item_table.items():
if data.category == category:
item_dict.setdefault(name, data)
return item_dict
item_table: Dict[str, RLItemData] = {
# Vendors
"Blacksmith": RLItemData("Vendors", 90_000, ItemClassification.progression),
"Enchantress": RLItemData("Vendors", 90_001, ItemClassification.progression),
"Architect": RLItemData("Vendors", 90_002, ItemClassification.useful),
# Classes
"Progressive Knights": RLItemData("Classes", 90_003, ItemClassification.useful, 2),
"Progressive Mages": RLItemData("Classes", 90_004, ItemClassification.useful, 2),
"Progressive Barbarians": RLItemData("Classes", 90_005, ItemClassification.useful, 2),
"Progressive Knaves": RLItemData("Classes", 90_006, ItemClassification.useful, 2),
"Progressive Shinobis": RLItemData("Classes", 90_007, ItemClassification.useful, 2),
"Progressive Miners": RLItemData("Classes", 90_008, ItemClassification.useful, 2),
"Progressive Liches": RLItemData("Classes", 90_009, ItemClassification.useful, 2),
"Progressive Spellthieves": RLItemData("Classes", 90_010, ItemClassification.useful, 2),
"Dragons": RLItemData("Classes", 90_096, ItemClassification.progression),
"Traitors": RLItemData("Classes", 90_097, ItemClassification.useful),
# Skills
"Health Up": RLItemData("Skills", 90_013, ItemClassification.progression_skip_balancing, 15),
"Mana Up": RLItemData("Skills", 90_014, ItemClassification.progression_skip_balancing, 15),
"Attack Up": RLItemData("Skills", 90_015, ItemClassification.progression_skip_balancing, 15),
"Magic Damage Up": RLItemData("Skills", 90_016, ItemClassification.progression_skip_balancing, 15),
"Armor Up": RLItemData("Skills", 90_017, ItemClassification.useful, 15),
"Equip Up": RLItemData("Skills", 90_018, ItemClassification.useful, 5),
"Crit Chance Up": RLItemData("Skills", 90_019, ItemClassification.useful, 5),
"Crit Damage Up": RLItemData("Skills", 90_020, ItemClassification.useful, 5),
"Down Strike Up": RLItemData("Skills", 90_021),
"Gold Gain Up": RLItemData("Skills", 90_022),
"Potion Efficiency Up": RLItemData("Skills", 90_023),
"Invulnerability Time Up": RLItemData("Skills", 90_024),
"Mana Cost Down": RLItemData("Skills", 90_025),
"Death Defiance": RLItemData("Skills", 90_026, ItemClassification.useful),
"Haggling": RLItemData("Skills", 90_027, ItemClassification.useful),
"Randomize Children": RLItemData("Skills", 90_028, ItemClassification.useful),
# Blueprints
"Progressive Blueprints": RLItemData("Blueprints", 90_055, ItemClassification.useful, 15),
"Squire Blueprints": RLItemData("Blueprints", 90_040, ItemClassification.useful),
"Silver Blueprints": RLItemData("Blueprints", 90_041, ItemClassification.useful),
"Guardian Blueprints": RLItemData("Blueprints", 90_042, ItemClassification.useful),
"Imperial Blueprints": RLItemData("Blueprints", 90_043, ItemClassification.useful),
"Royal Blueprints": RLItemData("Blueprints", 90_044, ItemClassification.useful),
"Knight Blueprints": RLItemData("Blueprints", 90_045, ItemClassification.useful),
"Ranger Blueprints": RLItemData("Blueprints", 90_046, ItemClassification.useful),
"Sky Blueprints": RLItemData("Blueprints", 90_047, ItemClassification.useful),
"Dragon Blueprints": RLItemData("Blueprints", 90_048, ItemClassification.useful),
"Slayer Blueprints": RLItemData("Blueprints", 90_049, ItemClassification.useful),
"Blood Blueprints": RLItemData("Blueprints", 90_050, ItemClassification.useful),
"Sage Blueprints": RLItemData("Blueprints", 90_051, ItemClassification.useful),
"Retribution Blueprints": RLItemData("Blueprints", 90_052, ItemClassification.useful),
"Holy Blueprints": RLItemData("Blueprints", 90_053, ItemClassification.useful),
"Dark Blueprints": RLItemData("Blueprints", 90_054, ItemClassification.useful),
# Runes
"Vault Runes": RLItemData("Runes", 90_060, ItemClassification.progression),
"Sprint Runes": RLItemData("Runes", 90_061, ItemClassification.progression),
"Vampire Runes": RLItemData("Runes", 90_062, ItemClassification.useful),
"Sky Runes": RLItemData("Runes", 90_063, ItemClassification.progression),
"Siphon Runes": RLItemData("Runes", 90_064, ItemClassification.useful),
"Retaliation Runes": RLItemData("Runes", 90_065),
"Bounty Runes": RLItemData("Runes", 90_066),
"Haste Runes": RLItemData("Runes", 90_067),
"Curse Runes": RLItemData("Runes", 90_068),
"Grace Runes": RLItemData("Runes", 90_069),
"Balance Runes": RLItemData("Runes", 90_070, ItemClassification.useful),
# Junk
"Triple Stat Increase": RLItemData("Filler", 90_030, weight=6),
"1000 Gold": RLItemData("Filler", 90_031, weight=3),
"3000 Gold": RLItemData("Filler", 90_032, weight=2),
"5000 Gold": RLItemData("Filler", 90_033, weight=1),
}
event_item_table: Dict[str, RLItemData] = {
"Defeat Khidr": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Alexander": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Ponce de Leon": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Herodotus": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Neo Khidr": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Alexander IV": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Ponce de Freon": RLItemData("Event", classification=ItemClassification.progression),
"Defeat Astrodotus": RLItemData("Event", classification=ItemClassification.progression),
"Defeat The Fountain": RLItemData("Event", classification=ItemClassification.progression),
}

View File

@@ -1,94 +0,0 @@
from typing import Dict, NamedTuple, Optional
from BaseClasses import Location
class RLLocation(Location):
game: str = "Rogue Legacy"
class RLLocationData(NamedTuple):
category: str
code: Optional[int] = None
def get_locations_by_category(category: str) -> Dict[str, RLLocationData]:
location_dict: Dict[str, RLLocationData] = {}
for name, data in location_table.items():
if data.category == category:
location_dict.setdefault(name, data)
return location_dict
location_table: Dict[str, RLLocationData] = {
# Manor Renovation
"Manor - Ground Road": RLLocationData("Manor", 91_000),
"Manor - Main Base": RLLocationData("Manor", 91_001),
"Manor - Main Bottom Window": RLLocationData("Manor", 91_002),
"Manor - Main Top Window": RLLocationData("Manor", 91_003),
"Manor - Main Rooftop": RLLocationData("Manor", 91_004),
"Manor - Left Wing Base": RLLocationData("Manor", 91_005),
"Manor - Left Wing Window": RLLocationData("Manor", 91_006),
"Manor - Left Wing Rooftop": RLLocationData("Manor", 91_007),
"Manor - Left Big Base": RLLocationData("Manor", 91_008),
"Manor - Left Big Upper 1": RLLocationData("Manor", 91_009),
"Manor - Left Big Upper 2": RLLocationData("Manor", 91_010),
"Manor - Left Big Windows": RLLocationData("Manor", 91_011),
"Manor - Left Big Rooftop": RLLocationData("Manor", 91_012),
"Manor - Left Far Base": RLLocationData("Manor", 91_013),
"Manor - Left Far Roof": RLLocationData("Manor", 91_014),
"Manor - Left Extension": RLLocationData("Manor", 91_015),
"Manor - Left Tree 1": RLLocationData("Manor", 91_016),
"Manor - Left Tree 2": RLLocationData("Manor", 91_017),
"Manor - Right Wing Base": RLLocationData("Manor", 91_018),
"Manor - Right Wing Window": RLLocationData("Manor", 91_019),
"Manor - Right Wing Rooftop": RLLocationData("Manor", 91_020),
"Manor - Right Big Base": RLLocationData("Manor", 91_021),
"Manor - Right Big Upper": RLLocationData("Manor", 91_022),
"Manor - Right Big Rooftop": RLLocationData("Manor", 91_023),
"Manor - Right High Base": RLLocationData("Manor", 91_024),
"Manor - Right High Upper": RLLocationData("Manor", 91_025),
"Manor - Right High Tower": RLLocationData("Manor", 91_026),
"Manor - Right Extension": RLLocationData("Manor", 91_027),
"Manor - Right Tree": RLLocationData("Manor", 91_028),
"Manor - Observatory Base": RLLocationData("Manor", 91_029),
"Manor - Observatory Telescope": RLLocationData("Manor", 91_030),
# Boss Rewards
"Castle Hamson Boss Reward": RLLocationData("Boss", 91_100),
"Forest Abkhazia Boss Reward": RLLocationData("Boss", 91_102),
"The Maya Boss Reward": RLLocationData("Boss", 91_104),
"Land of Darkness Boss Reward": RLLocationData("Boss", 91_106),
# Special Locations
"Jukebox": RLLocationData("Special", 91_200),
"Painting": RLLocationData("Special", 91_201),
"Cheapskate Elf's Game": RLLocationData("Special", 91_202),
"Carnival": RLLocationData("Special", 91_203),
# Diaries
**{f"Diary {i+1}": RLLocationData("Diary", 91_300 + i) for i in range(0, 25)},
# Chests
**{f"Castle Hamson - Chest {i+1}": RLLocationData("Chests", 91_600 + i) for i in range(0, 50)},
**{f"Forest Abkhazia - Chest {i+1}": RLLocationData("Chests", 91_700 + i) for i in range(0, 50)},
**{f"The Maya - Chest {i+1}": RLLocationData("Chests", 91_800 + i) for i in range(0, 50)},
**{f"Land of Darkness - Chest {i+1}": RLLocationData("Chests", 91_900 + i) for i in range(0, 50)},
**{f"Chest {i+1}": RLLocationData("Chests", 92_000 + i) for i in range(0, 200)},
# Fairy Chests
**{f"Castle Hamson - Fairy Chest {i+1}": RLLocationData("Fairies", 91_400 + i) for i in range(0, 15)},
**{f"Forest Abkhazia - Fairy Chest {i+1}": RLLocationData("Fairies", 91_450 + i) for i in range(0, 15)},
**{f"The Maya - Fairy Chest {i+1}": RLLocationData("Fairies", 91_500 + i) for i in range(0, 15)},
**{f"Land of Darkness - Fairy Chest {i+1}": RLLocationData("Fairies", 91_550 + i) for i in range(0, 15)},
**{f"Fairy Chest {i+1}": RLLocationData("Fairies", 92_200 + i) for i in range(0, 60)},
}
event_location_table: Dict[str, RLLocationData] = {
"Castle Hamson Boss Room": RLLocationData("Event"),
"Forest Abkhazia Boss Room": RLLocationData("Event"),
"The Maya Boss Room": RLLocationData("Event"),
"Land of Darkness Boss Room": RLLocationData("Event"),
"Fountain Room": RLLocationData("Event"),
}

View File

@@ -1,387 +0,0 @@
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions
from dataclasses import dataclass
class StartingGender(Choice):
"""
Determines the gender of your initial 'Sir Lee' character.
"""
display_name = "Starting Gender"
option_sir = 0
option_lady = 1
alias_male = 0
alias_female = 1
default = "random"
class StartingClass(Choice):
"""
Determines the starting class of your initial 'Sir Lee' character.
"""
display_name = "Starting Class"
option_knight = 0
option_mage = 1
option_barbarian = 2
option_knave = 3
option_shinobi = 4
option_miner = 5
option_spellthief = 6
option_lich = 7
default = 0
class NewGamePlus(Choice):
"""
Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not
recommended for those inexperienced to Rogue Legacy!
"""
display_name = "New Game Plus"
option_normal = 0
option_new_game_plus = 1
option_new_game_plus_2 = 2
alias_hard = 1
alias_brutal = 2
default = 0
class LevelScaling(Range):
"""
A percentage modifier for scaling enemy level as you continue throughout the castle. 100 means enemies will have
100% level scaling (normal). Setting this too high will result in enemies with absurdly high levels, you have been
warned.
"""
display_name = "Enemy Level Scaling Percentage"
range_start = 1
range_end = 300
default = 100
class FairyChestsPerZone(Range):
"""
Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat
bonuses can be found in Fairy Chests.
"""
display_name = "Fairy Chests Per Zone"
range_start = 0
range_end = 15
default = 1
class ChestsPerZone(Range):
"""
Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only
gold or stat bonuses can be found in Chests.
"""
display_name = "Chests Per Zone"
range_start = 20
range_end = 50
default = 20
class UniversalFairyChests(Toggle):
"""
Determines if fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2.
"""
display_name = "Universal Fairy Chests"
class UniversalChests(Toggle):
"""
Determines if non-fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2.
"""
display_name = "Universal Non-Fairy Chests"
class Vendors(Choice):
"""
Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked).
"""
display_name = "Vendors"
option_start_unlocked = 0
option_early = 1
option_normal = 2
option_anywhere = 3
default = 1
class Architect(Choice):
"""
Determines where the Architect sits in the item pool.
"""
display_name = "Architect"
option_start_unlocked = 0
option_early = 1
option_anywhere = 2
option_disabled = 3
alias_normal = 2
default = 2
class ArchitectFee(Range):
"""
Determines how large of a percentage the architect takes from the player when utilizing his services. 100 means he
takes all your gold. 0 means his services are free.
"""
display_name = "Architect Fee Percentage"
range_start = 0
range_end = 100
default = 40
class DisableCharon(Toggle):
"""
Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool.
"""
display_name = "Disable Charon"
class RequirePurchasing(DefaultOnToggle):
"""
Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before
equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account.
"""
display_name = "Require Purchasing"
class ProgressiveBlueprints(Toggle):
"""
Instead of shuffling blueprints randomly into the pool, blueprint unlocks are progressively unlocked. You would get
Squire first, then Knight, etc., until finally Dark.
"""
display_name = "Progressive Blueprints"
class GoldGainMultiplier(Choice):
"""
Adjusts the multiplier for gaining gold from all sources.
"""
display_name = "Gold Gain Multiplier"
option_normal = 0
option_quarter = 1
option_half = 2
option_double = 3
option_quadruple = 4
default = 0
class NumberOfChildren(Range):
"""
Determines the number of offspring you can choose from on the lineage screen after a death.
"""
display_name = "Number of Children"
range_start = 1
range_end = 5
default = 3
class AdditionalLadyNames(OptionSet):
"""
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
of names your children can have. The first value will also be your initial character's name depending on Starting
Gender.
"""
display_name = "Additional Lady Names"
class AdditionalSirNames(OptionSet):
"""
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
of names your children can have. The first value will also be your initial character's name depending on Starting
Gender.
"""
display_name = "Additional Sir Names"
class AllowDefaultNames(DefaultOnToggle):
"""
Determines if the default names defined in the vanilla game are allowed to be used. Warning: Your world will not
generate if the number of Additional Names defined is less than the Number of Children value.
"""
display_name = "Allow Default Names"
class CastleScaling(Range):
"""
Adjusts the scaling factor for how big a castle can be. Larger castles scale enemies quicker and also take longer
to generate. 100 means normal castle size.
"""
display_name = "Castle Size Scaling Percentage"
range_start = 50
range_end = 300
default = 100
class ChallengeBossKhidr(Choice):
"""
Determines if Neo Khidr replaces Khidr in their boss room.
"""
display_name = "Khidr"
option_vanilla = 0
option_challenge = 1
default = 0
class ChallengeBossAlexander(Choice):
"""
Determines if Alexander the IV replaces Alexander in their boss room.
"""
display_name = "Alexander"
option_vanilla = 0
option_challenge = 1
default = 0
class ChallengeBossLeon(Choice):
"""
Determines if Ponce de Freon replaces Ponce de Leon in their boss room.
"""
display_name = "Ponce de Leon"
option_vanilla = 0
option_challenge = 1
default = 0
class ChallengeBossHerodotus(Choice):
"""
Determines if Astrodotus replaces Herodotus in their boss room.
"""
display_name = "Herodotus"
option_vanilla = 0
option_challenge = 1
default = 0
class HealthUpPool(Range):
"""
Determines the number of Health Ups in the item pool.
"""
display_name = "Health Up Pool"
range_start = 0
range_end = 15
default = 15
class ManaUpPool(Range):
"""
Determines the number of Mana Ups in the item pool.
"""
display_name = "Mana Up Pool"
range_start = 0
range_end = 15
default = 15
class AttackUpPool(Range):
"""
Determines the number of Attack Ups in the item pool.
"""
display_name = "Attack Up Pool"
range_start = 0
range_end = 15
default = 15
class MagicDamageUpPool(Range):
"""
Determines the number of Magic Damage Ups in the item pool.
"""
display_name = "Magic Damage Up Pool"
range_start = 0
range_end = 15
default = 15
class ArmorUpPool(Range):
"""
Determines the number of Armor Ups in the item pool.
"""
display_name = "Armor Up Pool"
range_start = 0
range_end = 10
default = 10
class EquipUpPool(Range):
"""
Determines the number of Equip Ups in the item pool.
"""
display_name = "Equip Up Pool"
range_start = 0
range_end = 10
default = 10
class CritChanceUpPool(Range):
"""
Determines the number of Crit Chance Ups in the item pool.
"""
display_name = "Crit Chance Up Pool"
range_start = 0
range_end = 5
default = 5
class CritDamageUpPool(Range):
"""
Determines the number of Crit Damage Ups in the item pool.
"""
display_name = "Crit Damage Up Pool"
range_start = 0
range_end = 5
default = 5
class FreeDiaryOnGeneration(DefaultOnToggle):
"""
Allows the player to get a free diary check every time they regenerate the castle in the starting room.
"""
display_name = "Free Diary On Generation"
class AvailableClasses(OptionSet):
"""
List of classes that will be in the item pool to find. The upgraded form of the class will be added with it.
The upgraded form of your starting class will be available regardless.
"""
display_name = "Available Classes"
default = frozenset(
{"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
)
valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
@dataclass
class RLOptions(PerGameCommonOptions):
starting_gender: StartingGender
starting_class: StartingClass
available_classes: AvailableClasses
new_game_plus: NewGamePlus
fairy_chests_per_zone: FairyChestsPerZone
chests_per_zone: ChestsPerZone
universal_fairy_chests: UniversalFairyChests
universal_chests: UniversalChests
vendors: Vendors
architect: Architect
architect_fee: ArchitectFee
disable_charon: DisableCharon
require_purchasing: RequirePurchasing
progressive_blueprints: ProgressiveBlueprints
gold_gain_multiplier: GoldGainMultiplier
number_of_children: NumberOfChildren
free_diary_on_generation: FreeDiaryOnGeneration
khidr: ChallengeBossKhidr
alexander: ChallengeBossAlexander
leon: ChallengeBossLeon
herodotus: ChallengeBossHerodotus
health_pool: HealthUpPool
mana_pool: ManaUpPool
attack_pool: AttackUpPool
magic_damage_pool: MagicDamageUpPool
armor_pool: ArmorUpPool
equip_pool: EquipUpPool
crit_chance_pool: CritChanceUpPool
crit_damage_pool: CritDamageUpPool
allow_default_names: AllowDefaultNames
additional_lady_names: AdditionalLadyNames
additional_sir_names: AdditionalSirNames
death_link: DeathLink

View File

@@ -1,61 +0,0 @@
from typing import Any, Dict
from .Options import Architect, GoldGainMultiplier, Vendors
rl_options_presets: Dict[str, Dict[str, Any]] = {
# Example preset using only literal values.
"Unknown Fate": {
"progression_balancing": "random",
"accessibility": "random",
"starting_gender": "random",
"starting_class": "random",
"new_game_plus": "random",
"fairy_chests_per_zone": "random",
"chests_per_zone": "random",
"universal_fairy_chests": "random",
"universal_chests": "random",
"vendors": "random",
"architect": "random",
"architect_fee": "random",
"disable_charon": "random",
"require_purchasing": "random",
"progressive_blueprints": "random",
"gold_gain_multiplier": "random",
"number_of_children": "random",
"free_diary_on_generation": "random",
"khidr": "random",
"alexander": "random",
"leon": "random",
"herodotus": "random",
"health_pool": "random",
"mana_pool": "random",
"attack_pool": "random",
"magic_damage_pool": "random",
"armor_pool": "random",
"equip_pool": "random",
"crit_chance_pool": "random",
"crit_damage_pool": "random",
"allow_default_names": True,
"death_link": "random",
},
# A preset I actually use, using some literal values and some from the option itself.
"Limited Potential": {
"progression_balancing": "disabled",
"fairy_chests_per_zone": 2,
"starting_class": "random",
"chests_per_zone": 30,
"vendors": Vendors.option_normal,
"architect": Architect.option_disabled,
"gold_gain_multiplier": GoldGainMultiplier.option_half,
"number_of_children": 2,
"free_diary_on_generation": False,
"health_pool": 10,
"mana_pool": 10,
"attack_pool": 10,
"magic_damage_pool": 10,
"armor_pool": 5,
"equip_pool": 10,
"crit_chance_pool": 5,
"crit_damage_pool": 5,
}
}

View File

@@ -1,114 +0,0 @@
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import MultiWorld, Region, Entrance
from .Locations import RLLocation, location_table, get_locations_by_category
if TYPE_CHECKING:
from . import RLWorld
class RLRegionData(NamedTuple):
locations: Optional[List[str]]
region_exits: Optional[List[str]]
def create_regions(world: "RLWorld"):
regions: Dict[str, RLRegionData] = {
"Menu": RLRegionData(None, ["Castle Hamson"]),
"The Manor": RLRegionData([], []),
"Castle Hamson": RLRegionData([], ["Forest Abkhazia", "The Maya", "Land of Darkness",
"The Fountain Room", "The Manor"]),
"Forest Abkhazia": RLRegionData([], []),
"The Maya": RLRegionData([], []),
"Land of Darkness": RLRegionData([], []),
"The Fountain Room": RLRegionData([], None),
}
# Artificially stagger diary spheres for progression.
for diary in range(0, 25):
region: str
if 0 <= diary < 6:
region = "Castle Hamson"
elif 6 <= diary < 12:
region = "Forest Abkhazia"
elif 12 <= diary < 18:
region = "The Maya"
elif 18 <= diary < 24:
region = "Land of Darkness"
else:
region = "The Fountain Room"
regions[region].locations.append(f"Diary {diary + 1}")
# Manor & Special
for manor in get_locations_by_category("Manor").keys():
regions["The Manor"].locations.append(manor)
for special in get_locations_by_category("Special").keys():
regions["Castle Hamson"].locations.append(special)
# Boss Rewards
regions["Castle Hamson"].locations.append("Castle Hamson Boss Reward")
regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Reward")
regions["The Maya"].locations.append("The Maya Boss Reward")
regions["Land of Darkness"].locations.append("Land of Darkness Boss Reward")
# Events
regions["Castle Hamson"].locations.append("Castle Hamson Boss Room")
regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Room")
regions["The Maya"].locations.append("The Maya Boss Room")
regions["Land of Darkness"].locations.append("Land of Darkness Boss Room")
regions["The Fountain Room"].locations.append("Fountain Room")
# Chests
chests = int(world.options.chests_per_zone)
for i in range(0, chests):
if world.options.universal_chests:
regions["Castle Hamson"].locations.append(f"Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}")
regions["Land of Darkness"].locations.append(f"Chest {i + 1 + (chests * 3)}")
else:
regions["Castle Hamson"].locations.append(f"Castle Hamson - Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Chest {i + 1}")
regions["The Maya"].locations.append(f"The Maya - Chest {i + 1}")
regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}")
# Fairy Chests
chests = int(world.options.fairy_chests_per_zone)
for i in range(0, chests):
if world.options.universal_fairy_chests:
regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}")
regions["Land of Darkness"].locations.append(f"Fairy Chest {i + 1 + (chests * 3)}")
else:
regions["Castle Hamson"].locations.append(f"Castle Hamson - Fairy Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Fairy Chest {i + 1}")
regions["The Maya"].locations.append(f"The Maya - Fairy Chest {i + 1}")
regions["Land of Darkness"].locations.append(f"Land of Darkness - Fairy Chest {i + 1}")
# Set up the regions correctly.
for name, data in regions.items():
world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data))
world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson"))
world.get_entrance("The Manor").connect(world.get_region("The Manor"))
world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia"))
world.get_entrance("The Maya").connect(world.get_region("The Maya"))
world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness"))
world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room"))
def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData):
region = Region(name, player, multiworld)
if data.locations:
for loc_name in data.locations:
loc_data = location_table.get(loc_name)
location = RLLocation(player, loc_name, loc_data.code if loc_data else None, region)
region.locations.append(location)
if data.region_exits:
for exit in data.region_exits:
entrance = Entrance(player, exit, region)
region.exits.append(entrance)
return region

View File

@@ -1,117 +0,0 @@
from BaseClasses import CollectionState
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import RLWorld
def get_upgrade_total(world: "RLWorld") -> int:
return int(world.options.health_pool) + int(world.options.mana_pool) + \
int(world.options.attack_pool) + int(world.options.magic_damage_pool)
def get_upgrade_count(state: CollectionState, player: int) -> int:
return state.count("Health Up", player) + state.count("Mana Up", player) + \
state.count("Attack Up", player) + state.count("Magic Damage Up", player)
def has_vendors(state: CollectionState, player: int) -> bool:
return state.has_all({"Blacksmith", "Enchantress"}, player)
def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool:
return get_upgrade_count(state, player) >= amount
def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool:
return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100)))
def has_movement_rune(state: CollectionState, player: int) -> bool:
return state.has("Vault Runes", player) or state.has("Sprint Runes", player) or state.has("Sky Runes", player)
def has_fairy_progression(state: CollectionState, player: int) -> bool:
return state.has("Dragons", player) or (state.has("Enchantress", player) and has_movement_rune(state, player))
def has_defeated_castle(state: CollectionState, player: int) -> bool:
return state.has("Defeat Khidr", player) or state.has("Defeat Neo Khidr", player)
def has_defeated_forest(state: CollectionState, player: int) -> bool:
return state.has("Defeat Alexander", player) or state.has("Defeat Alexander IV", player)
def has_defeated_tower(state: CollectionState, player: int) -> bool:
return state.has("Defeat Ponce de Leon", player) or state.has("Defeat Ponce de Freon", player)
def has_defeated_dungeon(state: CollectionState, player: int) -> bool:
return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player)
def set_rules(world: "RLWorld", player: int):
# If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres.
if world.options.vendors == "normal":
world.get_location("Forest Abkhazia Boss Reward").access_rule = \
lambda state: has_vendors(state, player)
# Gate each manor location so everything isn't dumped into sphere 1.
manor_rules = {
"Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [
"Manor - Left Wing Window",
"Manor - Left Wing Rooftop",
"Manor - Right Wing Window",
"Manor - Right Wing Rooftop",
"Manor - Left Big Base",
"Manor - Right Big Base",
"Manor - Left Tree 1",
"Manor - Left Tree 2",
"Manor - Right Tree",
],
"Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [
"Manor - Left Big Upper 1",
"Manor - Left Big Upper 2",
"Manor - Left Big Windows",
"Manor - Left Big Rooftop",
"Manor - Left Far Base",
"Manor - Left Far Roof",
"Manor - Left Extension",
"Manor - Right Big Upper",
"Manor - Right Big Rooftop",
"Manor - Right Extension",
],
"Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [
"Manor - Right High Base",
"Manor - Right High Upper",
"Manor - Right High Tower",
"Manor - Observatory Base",
"Manor - Observatory Telescope",
]
}
# Set rules for manor locations.
for event, locations in manor_rules.items():
for location in locations:
world.get_location(location).access_rule = lambda state: state.has(event, player)
# Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests.
for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]:
fairy_location.access_rule = lambda state: has_fairy_progression(state, player)
# Region rules.
world.get_entrance("Forest Abkhazia").access_rule = \
lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player)
world.get_entrance("The Maya").access_rule = \
lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player)
world.get_entrance("Land of Darkness").access_rule = \
lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player)
world.get_entrance("The Fountain Room").access_rule = \
lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player)
# Win condition.
world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)

View File

@@ -1,243 +0,0 @@
from typing import List
from BaseClasses import Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
from .Locations import RLLocation, location_table
from .Options import RLOptions
from .Presets import rl_options_presets
from .Regions import create_regions
from .Rules import set_rules
class RLWeb(WebWorld):
theme = "stone"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, "
"multiworld, and related software.",
"English",
"rogue-legacy_en.md",
"rogue-legacy/en",
["Phar"]
)]
bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \
"report-an-issue---.md&title=%5BIssue%5D"
options_presets = rl_options_presets
class RLWorld(World):
"""
Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed
you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf.
But that's OK, because no one is perfect, and you don't have to be to succeed.
"""
game = "Rogue Legacy"
options_dataclass = RLOptions
options: RLOptions
topology_present = True
required_client_version = (0, 3, 5)
web = RLWeb()
item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None}
location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None}
def fill_slot_data(self) -> dict:
return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()])
def generate_early(self):
# Check validation of names.
additional_lady_names = len(self.options.additional_lady_names.value)
additional_sir_names = len(self.options.additional_sir_names.value)
if not self.options.allow_default_names:
if additional_lady_names < int(self.options.number_of_children):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_lady_names. "
f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}")
if additional_sir_names < int(self.options.number_of_children):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_sir_names. "
f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}")
def create_items(self):
item_pool: List[RLItem] = []
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
for name, data in item_table.items():
quantity = data.max_quantity
# Architect
if name == "Architect":
if self.options.architect == "disabled":
continue
if self.options.architect == "start_unlocked":
self.multiworld.push_precollected(self.create_item(name))
continue
if self.options.architect == "early":
self.multiworld.local_early_items[self.player]["Architect"] = 1
# Blacksmith and Enchantress
if name == "Blacksmith" or name == "Enchantress":
if self.options.vendors == "start_unlocked":
self.multiworld.push_precollected(self.create_item(name))
continue
if self.options.vendors == "early":
self.multiworld.local_early_items[self.player]["Blacksmith"] = 1
self.multiworld.local_early_items[self.player]["Enchantress"] = 1
# Haggling
if name == "Haggling" and self.options.disable_charon:
continue
# Blueprints
if data.category == "Blueprints":
# No progressive blueprints if progressive_blueprints are disabled.
if name == "Progressive Blueprints" and not self.options.progressive_blueprints:
continue
# No distinct blueprints if progressive_blueprints are enabled.
elif name != "Progressive Blueprints" and self.options.progressive_blueprints:
continue
# Classes
if data.category == "Classes":
if name == "Progressive Knights":
if "Knight" not in self.options.available_classes:
continue
if self.options.starting_class == "knight":
quantity = 1
if name == "Progressive Mages":
if "Mage" not in self.options.available_classes:
continue
if self.options.starting_class == "mage":
quantity = 1
if name == "Progressive Barbarians":
if "Barbarian" not in self.options.available_classes:
continue
if self.options.starting_class == "barbarian":
quantity = 1
if name == "Progressive Knaves":
if "Knave" not in self.options.available_classes:
continue
if self.options.starting_class == "knave":
quantity = 1
if name == "Progressive Miners":
if "Miner" not in self.options.available_classes:
continue
if self.options.starting_class == "miner":
quantity = 1
if name == "Progressive Shinobis":
if "Shinobi" not in self.options.available_classes:
continue
if self.options.starting_class == "shinobi":
quantity = 1
if name == "Progressive Liches":
if "Lich" not in self.options.available_classes:
continue
if self.options.starting_class == "lich":
quantity = 1
if name == "Progressive Spellthieves":
if "Spellthief" not in self.options.available_classes:
continue
if self.options.starting_class == "spellthief":
quantity = 1
if name == "Dragons":
if "Dragon" not in self.options.available_classes:
continue
if name == "Traitors":
if "Traitor" not in self.options.available_classes:
continue
# Skills
if name == "Health Up":
quantity = self.options.health_pool.value
elif name == "Mana Up":
quantity = self.options.mana_pool.value
elif name == "Attack Up":
quantity = self.options.attack_pool.value
elif name == "Magic Damage Up":
quantity = self.options.magic_damage_pool.value
elif name == "Armor Up":
quantity = self.options.armor_pool.value
elif name == "Equip Up":
quantity = self.options.equip_pool.value
elif name == "Crit Chance Up":
quantity = self.options.crit_chance_pool.value
elif name == "Crit Damage Up":
quantity = self.options.crit_damage_pool.value
# Ignore filler, it will be added in a later stage.
if data.category == "Filler":
continue
item_pool += [self.create_item(name) for _ in range(0, quantity)]
# Fill any empty locations with filler items.
while len(item_pool) < total_locations:
item_pool.append(self.create_item(self.get_filler_item_name()))
self.multiworld.itempool += item_pool
def get_filler_item_name(self) -> str:
fillers = get_items_by_category("Filler")
weights = [data.weight for data in fillers.values()]
return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
def create_item(self, name: str) -> RLItem:
data = item_table[name]
return RLItem(name, data.classification, data.code, self.player)
def create_event(self, name: str) -> RLItem:
data = event_item_table[name]
return RLItem(name, data.classification, data.code, self.player)
def set_rules(self):
set_rules(self, self.player)
def create_regions(self):
create_regions(self)
self._place_events()
def _place_events(self):
# Fountain
self.multiworld.get_location("Fountain Room", self.player).place_locked_item(
self.create_event("Defeat The Fountain"))
# Khidr / Neo Khidr
if self.options.khidr == "vanilla":
self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
self.create_event("Defeat Khidr"))
else:
self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
self.create_event("Defeat Neo Khidr"))
# Alexander / Alexander IV
if self.options.alexander == "vanilla":
self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
self.create_event("Defeat Alexander"))
else:
self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
self.create_event("Defeat Alexander IV"))
# Ponce de Leon / Ponce de Freon
if self.options.leon == "vanilla":
self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item(
self.create_event("Defeat Ponce de Leon"))
else:
self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item(
self.create_event("Defeat Ponce de Freon"))
# Herodotus / Astrodotus
if self.options.herodotus == "vanilla":
self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
self.create_event("Defeat Herodotus"))
else:
self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
self.create_event("Defeat Astrodotus"))

View File

@@ -1,34 +0,0 @@
# Rogue Legacy (PC)
## Where is the options page?
The [player options page for this game](../player-options) contains most of the options you need to
configure and export a config file. Some options can only be made in YAML, but an explanation can be found in the
[template yaml here](../../../static/generated/configs/Rogue%20Legacy.yaml).
## What does randomization do to this game?
Rogue Legacy Randomizer takes all the classes, skills, runes, and blueprints and spreads them out into chests, the manor
upgrade screen, bosses, and some special individual locations. The goal is to become powerful enough to defeat the four
zone bosses and then defeat The Fountain.
## What items and locations get shuffled?
All the skill upgrades, class upgrades, runes packs, and equipment packs are shuffled in the manor upgrade screen, diary
checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the finding of
stats less of a chore. Runes and Equipment are also grouped together.
Some additional locations that can contain items are the Jukebox, the Portraits, and the mini-game rewards.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## When the player receives an item, what happens?
When the player receives an item, your character will hold the item above their head and display it to the world. It's
good for business!
## What do I do if I encounter a bug with the game?
Please reach out to Phar#4444 on Discord or you can drop a bug report on the
[GitHub page for Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=report-an-issue---.md&title=%5BIssue%5D).

View File

@@ -1,35 +0,0 @@
# Rogue Legacy Randomizer Setup Guide
## Required Software
- Rogue Legacy Randomizer from the
[Rogue Legacy Randomizer Releases Page](https://github.com/ThePhar/RogueLegacyRandomizer/releases)
## Recommended Installation Instructions
Please read the README file on the
[Rogue Legacy Randomizer GitHub](https://github.com/ThePhar/RogueLegacyRandomizer/blob/master/README.md) page for
up-to-date installation instructions.
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
you can customize your options by visiting the [Rogue Legacy Options Page](/games/Rogue%20Legacy/player-options).
### Connect to the MultiServer
Once in game, press the start button and the AP connection screen should appear. You will fill out the hostname, port,
slot name, and password (if applicable). You should only need to fill out hostname, port, and password if the server
provides an alternative one to the default values.
### Play the game
Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen. Now
you're off to start your legacy!

View File

@@ -1,23 +0,0 @@
from typing import Dict
from . import RLTestBase
from ..Items import item_table
from ..Locations import location_table
class UniqueTest(RLTestBase):
@staticmethod
def test_item_ids_are_all_unique():
item_ids: Dict[int, str] = {}
for name, data in item_table.items():
assert data.code not in item_ids.keys(), f"'{name}': {data.code}, is not unique. " \
f"'{item_ids[data.code]}' also has this identifier."
item_ids[data.code] = name
@staticmethod
def test_location_ids_are_all_unique():
location_ids: Dict[int, str] = {}
for name, data in location_table.items():
assert data.code not in location_ids.keys(), f"'{name}': {data.code}, is not unique. " \
f"'{location_ids[data.code]}' also has this identifier."
location_ids[data.code] = name

View File

@@ -1,5 +0,0 @@
from test.bases import WorldTestBase
class RLTestBase(WorldTestBase):
game = "Rogue Legacy"

View File

@@ -386,7 +386,7 @@ coppper_slot_machine = skill_recipe(ModMachine.copper_slot_machine, ModSkill.luc
Forageable.salmonberry: 1, Material.clay: 1, Trash.joja_cola: 1}, ModNames.luck_skill)
gold_slot_machine = skill_recipe(ModMachine.gold_slot_machine, ModSkill.luck, 4, {MetalBar.gold: 15, ModMachine.copper_slot_machine: 1}, ModNames.luck_skill)
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 4, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 4, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 6, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 8, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes}

View File

@@ -6,7 +6,7 @@ id,name,classification,groups,mod_name
18,Greenhouse,progression,COMMUNITY_REWARD,
19,Glittering Boulder Removed,progression,COMMUNITY_REWARD,
20,Minecarts Repair,useful,COMMUNITY_REWARD,
21,Bus Repair,progression,COMMUNITY_REWARD,
21,Bus Repair,progression,"COMMUNITY_REWARD,DESERT_TRANSPORTATION",
22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD,
23,Stardrop,progression,,
24,Progressive Backpack,progression,,
@@ -63,8 +63,8 @@ id,name,classification,groups,mod_name
77,Combat Level,progression,SKILL_LEVEL_UP,
78,Earth Obelisk,progression,WIZARD_BUILDING,
79,Water Obelisk,progression,WIZARD_BUILDING,
80,Desert Obelisk,progression,WIZARD_BUILDING,
81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND",
80,Desert Obelisk,progression,"WIZARD_BUILDING,DESERT_TRANSPORTATION",
81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND,ISLAND_TRANSPORTATION",
82,Junimo Hut,useful,WIZARD_BUILDING,
83,Gold Clock,progression,WIZARD_BUILDING,
84,Progressive Coop,progression,BUILDING,
@@ -242,7 +242,7 @@ id,name,classification,groups,mod_name
257,Peach Sapling,progression,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
258,Banana Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
259,Mango Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
260,Boat Repair,progression,GINGER_ISLAND,
260,Boat Repair,progression,"GINGER_ISLAND,ISLAND_TRANSPORTATION",
261,Open Professor Snail Cave,progression,GINGER_ISLAND,
262,Island North Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
263,Island West Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
1 id name classification groups mod_name
6 18 Greenhouse progression COMMUNITY_REWARD
7 19 Glittering Boulder Removed progression COMMUNITY_REWARD
8 20 Minecarts Repair useful COMMUNITY_REWARD
9 21 Bus Repair progression COMMUNITY_REWARD COMMUNITY_REWARD,DESERT_TRANSPORTATION
10 22 Progressive Movie Theater progression,trap COMMUNITY_REWARD
11 23 Stardrop progression
12 24 Progressive Backpack progression
63 77 Combat Level progression SKILL_LEVEL_UP
64 78 Earth Obelisk progression WIZARD_BUILDING
65 79 Water Obelisk progression WIZARD_BUILDING
66 80 Desert Obelisk progression WIZARD_BUILDING WIZARD_BUILDING,DESERT_TRANSPORTATION
67 81 Island Obelisk progression WIZARD_BUILDING,GINGER_ISLAND WIZARD_BUILDING,GINGER_ISLAND,ISLAND_TRANSPORTATION
68 82 Junimo Hut useful WIZARD_BUILDING
69 83 Gold Clock progression WIZARD_BUILDING
70 84 Progressive Coop progression BUILDING
242 257 Peach Sapling progression RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY
243 258 Banana Sapling progression GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY
244 259 Mango Sapling progression GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY
245 260 Boat Repair progression GINGER_ISLAND GINGER_ISLAND,ISLAND_TRANSPORTATION
246 261 Open Professor Snail Cave progression GINGER_ISLAND
247 262 Island North Turtle progression GINGER_ISLAND,WALNUT_PURCHASE
248 263 Island West Turtle progression GINGER_ISLAND,WALNUT_PURCHASE

View File

@@ -33,6 +33,8 @@ class Group(enum.Enum):
SKILL_MASTERY = enum.auto()
BUILDING = enum.auto()
WIZARD_BUILDING = enum.auto()
DESERT_TRANSPORTATION = enum.auto()
ISLAND_TRANSPORTATION = enum.auto()
ARCADE_MACHINE_BUFFS = enum.auto()
BASE_RESOURCE = enum.auto()
WARP_TOTEM = enum.auto()

View File

@@ -1,61 +1,122 @@
from typing import Any
tww_options_presets: dict[str, dict[str, Any]] = {
"Tournament S7": {
"Tournament S8": {
"progression_dungeon_secrets": True,
"progression_combat_secret_caves": True,
"progression_short_sidequests": True,
"progression_long_sidequests": True,
"progression_spoils_trading": True,
"progression_big_octos_gunboats": True,
"progression_mail": True,
"progression_platforms_rafts": True,
"progression_submarines": True,
"progression_big_octos_gunboats": True,
"progression_expensive_purchases": True,
"progression_island_puzzles": True,
"progression_misc": True,
"randomize_mapcompass": "startwith",
"randomize_bigkeys": "startwith",
"required_bosses": True,
"num_required_bosses": 3,
"num_required_bosses": 4,
"included_dungeons": ["Forsaken Fortress"],
"chest_type_matches_contents": True,
"logic_obscurity": "hard",
"randomize_dungeon_entrances": True,
"randomize_starting_island": True,
"add_shortcut_warps_between_dungeons": True,
"start_inventory_from_pool": {
"Telescope": 1,
"Wind Waker": 1,
"Goddess Tingle Statue": 1,
"Earth Tingle Statue": 1,
"Wind Tingle Statue": 1,
"Wind's Requiem": 1,
"Ballad of Gales": 1,
"Command Melody": 1,
"Earth God's Lyric": 1,
"Wind God's Aria": 1,
"Song of Passing": 1,
"Progressive Magic Meter": 2,
"Triforce Shard 1": 1,
"Triforce Shard 2": 1,
"Triforce Shard 3": 1,
"Skull Necklace": 20,
"Golden Feather": 20,
"Knight's Crest": 10,
"Green Chu Jelly": 15,
"Nayru's Pearl": 1,
"Din's Pearl": 1,
},
"start_location_hints": ["Ganon's Tower - Maze Chest"],
"start_location_hints": [
"Windfall Island - Chu Jelly Juice Shop - Give 15 Blue Chu Jelly",
"Ganon's Tower - Maze Chest",
],
"exclude_locations": [
"Outset Island - Orca - Give 10 Knight's Crests",
"Outset Island - Great Fairy",
"Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly",
"Windfall Island - Mrs. Marie - Give 1 Joy Pendant",
"Windfall Island - Mrs. Marie - Give 21 Joy Pendants",
"Windfall Island - Mrs. Marie - Give 40 Joy Pendants",
"Windfall Island - Maggie's Father - Give 20 Skull Necklaces",
"Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers",
"Windfall Island - Lenzo's House - Become Lenzo's Assistant",
"Windfall Island - Lenzo's House - Bring Forest Firefly",
"Windfall Island - Sam - Decorate the Town",
"Windfall Island - Kamo - Full Moon Photo",
"Windfall Island - Linda and Anton",
"Dragon Roost Island - Secret Cave",
"Greatfish Isle - Hidden Chest",
"Mother and Child Isles - Inside Mother Isle",
"Fire Mountain - Cave - Chest",
"Fire Mountain - Lookout Platform Chest",
"Fire Mountain - Lookout Platform - Destroy the Cannons",
"Fire Mountain - Big Octo",
"Mailbox - Letter from Hoskit's Girlfriend",
"Headstone Island - Top of the Island",
"Headstone Island - Submarine",
"Earth Temple - Behind Curtain Next to Hammer Button",
"The Great Sea - Goron Trading Reward",
"The Great Sea - Withered Trees",
"Private Oasis - Big Octo",
"Boating Course - Raft",
"Boating Course - Cave",
"Stone Watcher Island - Cave",
"Stone Watcher Island - Lookout Platform Chest",
"Stone Watcher Island - Lookout Platform - Destroy the Cannons",
"Overlook Island - Cave",
"Bird's Peak Rock - Cave",
"Pawprint Isle - Wizzrobe Cave",
"Thorned Fairy Island - Great Fairy",
"Thorned Fairy Island - Northeastern Lookout Platform - Destroy the Cannons",
"Thorned Fairy Island - Southwestern Lookout Platform - Defeat the Enemies",
"Eastern Fairy Island - Great Fairy",
"Eastern Fairy Island - Lookout Platform - Defeat the Cannons and Enemies",
"Western Fairy Island - Great Fairy",
"Southern Fairy Island - Great Fairy",
"Northern Fairy Island - Great Fairy",
"Western Fairy Island - Lookout Platform",
"Tingle Island - Ankle - Reward for All Tingle Statues",
"Tingle Island - Big Octo",
"Diamond Steppe Island - Big Octo",
"Rock Spire Isle - Cave",
"Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item",
"Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item",
"Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item",
"Rock Spire Isle - Western Lookout Platform - Destroy the Cannons",
"Rock Spire Isle - Eastern Lookout Platform - Destroy the Cannons",
"Rock Spire Isle - Center Lookout Platform",
"Rock Spire Isle - Southeast Gunboat",
"Shark Island - Cave",
"Horseshoe Island - Northwestern Lookout Platform",
"Horseshoe Island - Southeastern Lookout Platform",
"Flight Control Platform - Submarine",
"Star Island - Cave",
"Star Island - Lookout Platform",
"Star Belt Archipelago - Lookout Platform",
"Five-Star Isles - Lookout Platform - Destroy the Cannons",
"Five-Star Isles - Raft",
"Five-Star Isles - Submarine",
"Seven-Star Isles - Center Lookout Platform",
"Seven-Star Isles - Northern Lookout Platform",
"Seven-Star Isles - Southern Lookout Platform",
"Seven-Star Isles - Big Octo",
"Cyclops Reef - Lookout Platform - Defeat the Enemies",
"Two-Eye Reef - Lookout Platform",
"Two-Eye Reef - Big Octo Great Fairy",
"Five-Eye Reef - Lookout Platform",
"Six-Eye Reef - Lookout Platform - Destroy the Cannons",
"Six-Eye Reef - Submarine",
],
},
"Miniblins 2025": {

View File

@@ -76,10 +76,11 @@ at least normal.
A few presets are available on the [player options page](../player-options) for your convenience.
- **Tournament S7**: These are (as close to as possible) the settings used in the WWR Racing Server's
[Season 7 Tournament](https://docs.google.com/document/d/1mJj7an-DvpYilwNt-DdlFOy1fz5_NMZaPZvHeIekplc).
The preset features 3 required bosses and hard obscurity difficulty, and while the list of enabled progression options
may seem intimidating, the preset also excludes several locations.
- **Tournament S8**: These are (as close to as possible) the settings used in the WWR Racing Server's
[Season 8 Tournament](https://docs.google.com/document/d/1b8F5DL3P5fgsQC_URiwhpMfqTpsGh2M-KmtTdXVigh4).
The preset features 4 required bosses (with Helmaroc King guaranteed required), dungeon entrance rando, hard obscurity
difficulty, and a variety of overworld checks. While the list of enabled progression options may seem intimidating,
the preset also excludes several locations and starts you with a handful of items.
- **Miniblins 2025**: These are (as close to as possible) the settings used in the WWR Racing Server's
[2025 Season of Miniblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This
preset is great if you're new to Wind Waker! There aren't too many locations in the world, and you only need to

View File

@@ -110,6 +110,14 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
else:
filler_pool.extend([item] * data.quantity)
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
if world.options.sword_mode == "start_with_sword":
precollected_items.append("Progressive Sword")
progression_pool.remove("Progressive Sword")
# Or, if it's swordless mode, remove all swords from the item pool.
elif world.options.sword_mode == "swordless":
useful_pool = [item for item in useful_pool if item != "Progressive Sword"]
# Assign useful and filler items to item pools in the world.
world.random.shuffle(useful_pool)
world.random.shuffle(filler_pool)
@@ -141,17 +149,6 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
pool.extend(progression_pool)
num_items_left_to_place -= len(progression_pool)
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
if world.options.sword_mode == "start_with_sword":
precollected_items.append("Progressive Sword")
num_items_left_to_place += 1
pool.remove("Progressive Sword")
# Or, if it's swordless mode, remove all swords from the item pool.
elif world.options.sword_mode == "swordless":
while "Progressive Sword" in pool:
num_items_left_to_place += 1
pool.remove("Progressive Sword")
# Place useful items, then filler items to fill out the remaining locations.
pool.extend([world.get_filler_item_name(strict=False) for _ in range(num_items_left_to_place)])

View File

@@ -496,70 +496,74 @@ class WargrooveContext(CommonContext):
async def game_watcher(ctx: WargrooveContext):
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file == "deathLinkSend" and ctx.has_death_link:
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
failed_mission = f.read()
if ctx.slot is not None:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} failed {failed_mission}")
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("victory") > -1:
victory = True
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSacrifice" or file == "unitSacrificeAI":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSacrificeAI":
stored_units_key = ctx.ai_stored_units_key
try:
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file == "deathLinkSend" and ctx.has_death_link:
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
unit_class = f.read()
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "add", "value": [unit_class[:64]]}]}]
await ctx.send_msgs(message)
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSummonRequestAI" or file == "unitSummonRequest":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSummonRequestAI":
stored_units_key = ctx.ai_stored_units_key
with open(os.path.join(ctx.game_communication_path, "unitSummonResponse"), 'w') as f:
if stored_units_key in ctx.stored_data:
stored_units = ctx.stored_data[stored_units_key]
if stored_units is None:
stored_units = []
wg1_stored_units = [unit for unit in stored_units if unit in ctx.unit_classes]
if len(wg1_stored_units) != 0:
summoned_unit = random.choice(wg1_stored_units)
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "remove", "value": summoned_unit[:64]}]}]
await ctx.send_msgs(message)
f.write(summoned_unit)
os.remove(os.path.join(ctx.game_communication_path, file))
failed_mission = f.read()
if ctx.slot is not None:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} failed {failed_mission}")
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("victory") > -1:
victory = True
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSacrifice" or file == "unitSacrificeAI":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSacrificeAI":
stored_units_key = ctx.ai_stored_units_key
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
unit_class = f.read()
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "add", "value": [unit_class[:64]]}]}]
await ctx.send_msgs(message)
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSummonRequestAI" or file == "unitSummonRequest":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSummonRequestAI":
stored_units_key = ctx.ai_stored_units_key
with open(os.path.join(ctx.game_communication_path, "unitSummonResponse"), 'w') as f:
if stored_units_key in ctx.stored_data:
stored_units = ctx.stored_data[stored_units_key]
if stored_units is None:
stored_units = []
wg1_stored_units = [unit for unit in stored_units if unit in ctx.unit_classes]
if len(wg1_stored_units) != 0:
summoned_unit = random.choice(wg1_stored_units)
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "remove", "value": summoned_unit[:64]}]}]
await ctx.send_msgs(message)
f.write(summoned_unit)
os.remove(os.path.join(ctx.game_communication_path, file))
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
except Exception as err:
logger.warn("Exception in communication thread, a check may not have been sent: " + str(err))
def print_error_and_close(msg):