forked from mirror/Archipelago
Compare commits
22 Commits
0.6.2-rc3
...
revert-516
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7bca84e79 | ||
|
|
f128f2036c | ||
|
|
6af34b66fb | ||
|
|
2974f7d11f | ||
|
|
edc0c89753 | ||
|
|
b1ff55dd06 | ||
|
|
f4b5422f66 | ||
|
|
d4ebace99f | ||
|
|
95e09c8e2a | ||
|
|
4623d59206 | ||
|
|
e68b1ad428 | ||
|
|
072e2ece15 | ||
|
|
11130037fe | ||
|
|
ba66ef14cc | ||
|
|
8aacc23882 | ||
|
|
03e5fd3dae | ||
|
|
da52598c08 | ||
|
|
52389731eb | ||
|
|
21864f6f95 | ||
|
|
00f8625280 | ||
|
|
c34e29c712 | ||
|
|
e0ae3359f1 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
4
Main.py
4
Main.py
@@ -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:
|
||||
|
||||
21
NetUtils.py
21
NetUtils.py
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
1
Utils.py
1
Utils.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
8
kvui.py
8
kvui.py
@@ -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)
|
||||
|
||||
|
||||
1
setup.py
1
setup.py
@@ -63,7 +63,6 @@ non_apworlds: set[str] = {
|
||||
"Adventure",
|
||||
"ArchipIDLE",
|
||||
"Archipelago",
|
||||
"Clique",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
}))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 → Options → 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.
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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*~~
|
||||
@@ -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*~~
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"))
|
||||
@@ -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).
|
||||
@@ -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!
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class RLTestBase(WorldTestBase):
|
||||
game = "Rogue Legacy"
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
@@ -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()
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)])
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user