forked from mirror/Archipelago
Merge branch 'main' into player-tracker
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-windows-amd64.zip -OutFile sni.zip
|
||||
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0/win-x64.zip -OutFile enemizer.zip
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -152,10 +152,17 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
#minecraft server stuff
|
||||
# minecraft server stuff
|
||||
jdk*/
|
||||
minecraft*/
|
||||
minecraft_versions.json
|
||||
|
||||
#pyenv
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# OS General Files
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Thumbs.db
|
||||
[Dd]esktop.ini
|
||||
|
||||
@@ -1191,19 +1191,19 @@ class Item:
|
||||
|
||||
@property
|
||||
def advancement(self) -> bool:
|
||||
return bool(self.classification & ItemClassification.progression)
|
||||
return ItemClassification.progression in self.classification
|
||||
|
||||
@property
|
||||
def skip_in_prog_balancing(self) -> bool:
|
||||
return self.classification == ItemClassification.progression_skip_balancing
|
||||
return ItemClassification.progression_skip_balancing in self.classification
|
||||
|
||||
@property
|
||||
def useful(self) -> bool:
|
||||
return bool(self.classification & ItemClassification.useful)
|
||||
return ItemClassification.useful in self.classification
|
||||
|
||||
@property
|
||||
def trap(self) -> bool:
|
||||
return bool(self.classification & ItemClassification.trap)
|
||||
return ItemClassification.trap in self.classification
|
||||
|
||||
@property
|
||||
def flags(self) -> int:
|
||||
|
||||
12
Fill.py
12
Fill.py
@@ -42,8 +42,16 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
while items_to_place:
|
||||
# if we have run out of locations to fill,break out of this loop
|
||||
if not locations:
|
||||
unplaced_items += items_to_place
|
||||
break
|
||||
item_to_place = items_to_place.pop(0)
|
||||
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||
if world.accessibility[item_to_place.player] == 'minimal':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) \
|
||||
@@ -54,7 +62,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
for i, location in enumerate(locations):
|
||||
if (not single_player_placement or location.player == item_to_place.player) \
|
||||
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||
# poping by index is faster than removing by content,
|
||||
# popping by index is faster than removing by content,
|
||||
spot_to_fill = locations.pop(i)
|
||||
# skipping a scan for the element
|
||||
break
|
||||
|
||||
66
Generate.py
66
Generate.py
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
@@ -7,6 +9,7 @@ from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||
import os
|
||||
from collections import Counter
|
||||
import string
|
||||
import enum
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -25,7 +28,38 @@ from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
|
||||
categories = set(AutoWorldRegister.world_types)
|
||||
|
||||
class PlandoSettings(enum.IntFlag):
|
||||
items = 0b0001
|
||||
connections = 0b0010
|
||||
texts = 0b0100
|
||||
bosses = 0b1000
|
||||
|
||||
@classmethod
|
||||
def from_option_string(cls, option_string: str) -> PlandoSettings:
|
||||
result = cls(0)
|
||||
for part in option_string.split(","):
|
||||
part = part.strip().lower()
|
||||
if part:
|
||||
result = cls._handle_part(part, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_set(cls, option_set: Set[str]) -> PlandoSettings:
|
||||
result = cls(0)
|
||||
for part in option_set:
|
||||
result = cls._handle_part(part, result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _handle_part(cls, part: str, base: PlandoSettings) -> PlandoSettings:
|
||||
try:
|
||||
part = cls[part]
|
||||
except Exception as e:
|
||||
raise KeyError(f"{part} is not a recognized name for a plando module. "
|
||||
f"Known options: {', '.join(flag.name for flag in cls)}") from e
|
||||
else:
|
||||
return base | part
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
@@ -64,7 +98,7 @@ def mystery_argparse():
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
if not os.path.isabs(args.meta_file_path):
|
||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||
args.plando: Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||
args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando)
|
||||
return args, options
|
||||
|
||||
|
||||
@@ -127,7 +161,7 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
args.multi = max(player_id-1, args.multi)
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
||||
f"{', '.join(args.plando)}")
|
||||
f"{args.plando}")
|
||||
|
||||
if not weights_cache:
|
||||
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||
@@ -403,7 +437,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
elif "bosses" in plando_options:
|
||||
elif PlandoSettings.bosses in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
@@ -452,7 +486,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||
setattr(ret, option_key, option(option.default))
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",))):
|
||||
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
@@ -465,17 +499,11 @@ def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",
|
||||
if tuplize_version(version) > version_tuple:
|
||||
raise Exception(f"Settings reports required version of generator is at least {version}, "
|
||||
f"however generator is of version {__version__}")
|
||||
required_plando_options = requirements.get("plando", "")
|
||||
if required_plando_options:
|
||||
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
|
||||
required_plando_options -= plando_options
|
||||
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
|
||||
if required_plando_options not in plando_options:
|
||||
if required_plando_options:
|
||||
if len(required_plando_options) == 1:
|
||||
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
|
||||
f"which is not enabled.")
|
||||
else:
|
||||
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
|
||||
f"which are not enabled.")
|
||||
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
|
||||
f"which is not enabled.")
|
||||
|
||||
ret = argparse.Namespace()
|
||||
for option_key in Options.per_game_common_options:
|
||||
@@ -504,12 +532,12 @@ def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",
|
||||
# skip setting this option if already set from common_options, defaulting to root option
|
||||
if not (option_key in Options.common_options and option_key not in game_weights):
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
if "items" in plando_options:
|
||||
if PlandoSettings.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
if "connections" in plando_options:
|
||||
if PlandoSettings.connections in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
@@ -629,7 +657,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
ret.plando_texts = {}
|
||||
if "texts" in plando_options:
|
||||
if PlandoSettings.texts in plando_options:
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
@@ -641,7 +669,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
ret.plando_connections = []
|
||||
if "connections" in plando_options:
|
||||
if PlandoSettings.connections in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
|
||||
@@ -47,7 +47,7 @@ def main():
|
||||
|
||||
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
|
||||
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
|
||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
help='Path to an ALttP Japan(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
|
||||
@@ -1263,4 +1263,4 @@ class ToolTips(object):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -766,7 +766,7 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
|
||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||
"""register any locations that are in the multidata"""
|
||||
all_locations = set(ctx.locations[slot])
|
||||
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1))
|
||||
register_location_checks(ctx, team, slot, all_locations)
|
||||
update_checked_locations(ctx, team, slot)
|
||||
|
||||
@@ -779,7 +779,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
||||
if values[1] == slot:
|
||||
all_locations[source_slot].add(location_id)
|
||||
|
||||
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
|
||||
ctx.notify_all("%s (Team #%d) has collected their items from other worlds." % (ctx.player_names[(team, slot)], team + 1))
|
||||
for source_player, location_ids in all_locations.items():
|
||||
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
||||
update_checked_locations(ctx, team, source_player)
|
||||
@@ -1106,7 +1106,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return self.ctx.commandprocessor(command)
|
||||
|
||||
def _cmd_players(self) -> bool:
|
||||
"""Get information about connected and missing players"""
|
||||
"""Get information about connected and missing players."""
|
||||
if len(self.ctx.player_names) < 10:
|
||||
self.ctx.notify_all(get_players_string(self.ctx))
|
||||
else:
|
||||
@@ -1118,8 +1118,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(get_status_string(self.ctx, self.client.team))
|
||||
return True
|
||||
|
||||
def _cmd_release(self) -> bool:
|
||||
"""Sends remaining items in your world to their recipients."""
|
||||
return self._cmd_forfeit()
|
||||
|
||||
def _cmd_forfeit(self) -> bool:
|
||||
"""Surrender and send your remaining items out to their recipients"""
|
||||
"""Surrender and send your remaining items out to their recipients. Use release in the future."""
|
||||
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
@@ -1128,7 +1132,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return True
|
||||
elif "disabled" in self.ctx.forfeit_mode:
|
||||
self.output(
|
||||
"Sorry, client forfeiting has been disabled on this server. You can ask the server admin for a /forfeit")
|
||||
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
|
||||
return False
|
||||
else: # is auto or goal
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
@@ -1136,8 +1140,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return True
|
||||
else:
|
||||
self.output(
|
||||
"Sorry, client forfeiting requires you to have beaten the game on this server."
|
||||
" You can ask the server admin for a /forfeit")
|
||||
"Sorry, client item releasing requires you to have beaten the game on this server."
|
||||
" You can ask the server admin for a /release")
|
||||
return False
|
||||
|
||||
def _cmd_collect(self) -> bool:
|
||||
@@ -1698,43 +1702,48 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(f"Could not find player {player_name} to collect")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_release(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player to their intended recipients."""
|
||||
return self._cmd_forfeit(player_name)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forfeit(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player to their intended recipients"""
|
||||
"""Send out the remaining items from a player to their intended recipients."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
forfeit_player(self.ctx, team, slot)
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to forfeit")
|
||||
self.output(f"Could not find player {player_name} to release")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_allow_forfeit(self, player_name: str) -> bool:
|
||||
"""Allow the specified player to use the !forfeit command"""
|
||||
"""Allow the specified player to use the !release command."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
self.ctx.allow_forfeits[(team, slot)] = True
|
||||
self.output(f"Player {player_name} is now allowed to use the !forfeit command at any time.")
|
||||
self.output(f"Player {player_name} is now allowed to use the !release command at any time.")
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to allow the !forfeit command for.")
|
||||
self.output(f"Could not find player {player_name} to allow the !release command for.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forbid_forfeit(self, player_name: str) -> bool:
|
||||
""""Disallow the specified player from using the !forfeit command"""
|
||||
""""Disallow the specified player from using the !release command."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
self.ctx.allow_forfeits[(team, slot)] = False
|
||||
self.output(
|
||||
f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.")
|
||||
f"Player {player_name} has to follow the server restrictions on use of the !release command.")
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
|
||||
self.output(f"Could not find player {player_name} to forbid the !release command for.")
|
||||
return False
|
||||
|
||||
def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, *item_name: str) -> bool:
|
||||
|
||||
8
Patch.py
8
Patch.py
@@ -178,14 +178,14 @@ preferred_endings = {
|
||||
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
||||
if game == GAME_ALTTP:
|
||||
from worlds.alttp.Rom import JAP10HASH as HASH
|
||||
from worlds.alttp.Rom import LTTPJPN10HASH as HASH
|
||||
elif game == GAME_SM:
|
||||
from worlds.sm.Rom import JAP10HASH as HASH
|
||||
from worlds.sm.Rom import SMJUHASH as HASH
|
||||
elif game == GAME_SOE:
|
||||
from worlds.soe.Patch import USHASH as HASH
|
||||
elif game == GAME_SMZ3:
|
||||
from worlds.alttp.Rom import JAP10HASH as ALTTPHASH
|
||||
from worlds.sm.Rom import JAP10HASH as SMHASH
|
||||
from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH
|
||||
from worlds.sm.Rom import SMJUHASH as SMHASH
|
||||
HASH = ALTTPHASH + SMHASH
|
||||
else:
|
||||
raise RuntimeError(f"Selected game {game} for base rom not found.")
|
||||
|
||||
@@ -141,6 +141,11 @@ def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/glossary/<string:lang>/')
|
||||
def terms(lang):
|
||||
return render_template("glossary.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
def view_seed(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
|
||||
@@ -12,7 +12,7 @@ def allowed_file(filename):
|
||||
return filename.endswith(('.txt', ".yaml", ".zip"))
|
||||
|
||||
|
||||
from Generate import roll_settings
|
||||
from Generate import roll_settings, PlandoSettings
|
||||
from Utils import parse_yamls
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
||||
def roll_options(options: Dict[str, Union[dict, str]],
|
||||
plando_options: Set[str] = frozenset({"bosses", "items", "connections", "texts"})) -> \
|
||||
Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
|
||||
plando_options = set(plando_options)
|
||||
plando_options = PlandoSettings.from_set(set(plando_options))
|
||||
results = {}
|
||||
rolled_results = {}
|
||||
for filename, text in options.items():
|
||||
|
||||
@@ -12,7 +12,7 @@ from flask import request, flash, redirect, url_for, session, render_template
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
from Generate import handle_name
|
||||
from Generate import handle_name, PlandoSettings
|
||||
import pickle
|
||||
|
||||
from .models import *
|
||||
@@ -114,13 +114,13 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
|
||||
|
||||
erargs = parse_arguments(['--multi', str(playercount)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
||||
erargs.spoiler = 0 if race else 2
|
||||
erargs.race = race
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = target.name
|
||||
erargs.teams = 1
|
||||
erargs.plando_options = ", ".join(plando_options)
|
||||
erargs.plando_options = PlandoSettings.from_set(plando_options)
|
||||
|
||||
name_counter = Counter()
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
flask>=2.1.2
|
||||
pony>=0.7.16
|
||||
waitress>=2.1.1
|
||||
flask-caching>=1.11.1
|
||||
flask-caching>=2.0.0
|
||||
Flask-Compress>=1.12
|
||||
Flask-Limiter>=2.4.6
|
||||
Flask-Limiter>=2.5.0
|
||||
bokeh>=2.4.3
|
||||
@@ -49,6 +49,12 @@ If you are ready to start randomizing games, or want to start playing your favor
|
||||
our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer
|
||||
any questions you might have.
|
||||
|
||||
## What are some common terms I should know?
|
||||
|
||||
As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms
|
||||
and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common
|
||||
to Archipelago and its specific systems please see the [Glossary](/glossary/en).
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
|
||||
The best way to get started is to take a look at our code on GitHub
|
||||
|
||||
94
WebHostLib/static/assets/faq/glossary_en.md
Normal file
94
WebHostLib/static/assets/faq/glossary_en.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Multiworld Glossary
|
||||
|
||||
There are a lot of common terms used when playing in different game randomizer communities and in multiworld as well.
|
||||
This document serves as a lookup for common terms that may be used by users in the community or in various other
|
||||
documentation.
|
||||
|
||||
## Item
|
||||
Items are what get shuffled around in your world or other worlds that you then receive. This could be a sword, a stat
|
||||
upgrade, a spell, or any other potential receivable for your game.
|
||||
|
||||
## Location
|
||||
Locations are where items are placed in your game. Whenever you interact with a location, you or another player will
|
||||
then receive an item. A location could be a chest, an enemy drop, a shop purchase, or any other interactable that can
|
||||
contain items in your game.
|
||||
|
||||
## Check
|
||||
A check is a common term for when you "check", or pick up, a location. In terms of Archipelago this is usually used for
|
||||
when a player goes to a location and sends its item, or "checks" the location. Players will often reference their now
|
||||
randomized locations as checks.
|
||||
|
||||
## Slot
|
||||
A slot is the player name and number assigned during generation. The number of slots is equal to the number of players,
|
||||
or "worlds", created. Each name must be unique as these are used to identify the slot user.
|
||||
|
||||
## World
|
||||
World in terms of Archipelago can mean multiple things and is used interchangeably in many situations.
|
||||
* During gameplay, a world is a single instance of a game, occupying one player "slot". However,
|
||||
Archipelago allows multiple players to connect to the same slot; then those players can share a world
|
||||
and complete it cooperatively. For games with native cooperative play, you can also play together and
|
||||
share a world that way, usually with only one player connected to the multiworld.
|
||||
* On the programming side, a world typically represents the package that integrates Archipelago with a
|
||||
particular game. For example this could be the entire `worlds/factorio` directory.
|
||||
|
||||
## RNG
|
||||
Acronym for "Random Number Generator." Archipelago uses its own custom Random object with a unique seed per generation,
|
||||
or, if running from source, a seed can be supplied and this seed will control all randomization during generation as all
|
||||
game worlds will have access to it.
|
||||
|
||||
## Seed
|
||||
A "seed" is a number used to initialize a pseudorandom number generator. Whenever you generate a new game on Archipelago
|
||||
this is a new "seed" as it has unique item placement, and you can create multiple "rooms" on the Archipelago site from a
|
||||
single seed. Using the same seed results in the random placement being the same.
|
||||
|
||||
## Room
|
||||
Whenever you generate a seed on the Archipelago website you will be put on a seed page that contains all the seed info
|
||||
with a link to the spoiler if one exists and will show how many unique rooms exist per seed. Each room has its own
|
||||
unique identifier that is separate from the seed. The room page is where you can find information to connect to the
|
||||
multiworld and download any patches if necessary. If you have a particularly fun or interesting seed, and you want to
|
||||
share it with somebody you can link them to this seed page, where they can generate a new room to play it! For seeds
|
||||
generated with race mode enabled, the seed page will only show rooms created by the unique user so the seed page is
|
||||
perfectly safe to share for racing purposes.
|
||||
|
||||
## Logic
|
||||
Base behavior of all seeds generated by Archipelago is they are expected to be completable based on the requirements of
|
||||
the settings. This is done by using "logic" in order to determine valid locations to place items while still being able
|
||||
to reach said location without this item. For the purposes of the randomizer a location is considered "in logic" if you
|
||||
can reach it with your current toolset of items or skills based on settings. Some players are able to obtain locations
|
||||
"out of logic" by performing various glitches or tricks that the settings may not account for and tend to mention this
|
||||
when sending out an item they obtained this way.
|
||||
|
||||
## Progression
|
||||
Certain items will allow access to more locations and are considered progression items as they "progress" the seed.
|
||||
|
||||
## Trash
|
||||
A term used for "filler" items that have no bearing on the generation and are either marginally useful for the player
|
||||
or useless. These items can be very useful depending on the player but are never very important and as such are usually
|
||||
termed trash.
|
||||
|
||||
## Burger King / BK Mode
|
||||
A term used in multiworlds when a player is unable to continue to progress and is awaiting an item. The term came to be
|
||||
after a player, allegedly, was unable to progress during a multiworld and went to Burger King while waiting to receive
|
||||
items from other players.
|
||||
|
||||
* "Logical BK" is when the player is unable to progress according to the settings of their game but may still be able to do
|
||||
things that would be "out of logic" by the generation.
|
||||
|
||||
* "Hard / full BK" is when the player is completely unable to progress even with tricks they may know and are unable to
|
||||
continue to play, aside from doing something like killing enemies for experience or money.
|
||||
|
||||
## Sphere
|
||||
Archipelago calculates the game playthrough by using a "sphere" system where it has a state for each player and checks
|
||||
to see what the players are able to reach with their current items. Any location that is reachable with the current
|
||||
state of items is a "sphere." For the purposes of Archipelago it starts playthrough calculation by distributing sphere 0
|
||||
items which are items that are either forced in the player's inventory by the game or placed in the `start_inventory` in
|
||||
their settings. Sphere 1 is then all accessible locations the players can reach with all the items they received from
|
||||
sphere 0, or their starting inventory. The playthrough continues in this fashion calculating a number of spheres until
|
||||
all players have completed their goal.
|
||||
|
||||
## Scouts / Scouting
|
||||
In some games there are locations that have visible items even if the item itself is unobtainable at the current time.
|
||||
Some games utilize a scouting feature where when the player "sees" the item it will give a free hint for the item in the
|
||||
client letting the players know what the exact item is, since if the item was for that game it would know but the item
|
||||
being foreign is a lot harder to represent visually.
|
||||
|
||||
53
WebHostLib/static/assets/glossary.js
Normal file
53
WebHostLib/static/assets/glossary.js
Normal file
@@ -0,0 +1,53 @@
|
||||
window.addEventListener('load', () => {
|
||||
const tutorialWrapper = document.getElementById('glossary-wrapper');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, the glossary page is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the glossary.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
|
||||
`glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
|
||||
for (let i=0; i < headers.length; i++){
|
||||
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
|
||||
headers[i].setAttribute('id', headerId);
|
||||
headers[i].addEventListener('click', () =>
|
||||
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
|
||||
}
|
||||
|
||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
||||
if (scrollTargetIndex > -1) {
|
||||
try{
|
||||
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
|
||||
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
17
WebHostLib/templates/glossary.html
Normal file
17
WebHostLib/templates/glossary.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Glossary</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/glossary.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="glossary-wrapper" data-lang="{{ lang }}" class="markdown">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -26,6 +26,7 @@
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="/weighted-settings">Weighted Settings Page</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Game Info Pages</h2>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 246 KiB |
@@ -8,6 +8,15 @@ flowchart LR
|
||||
CC[CommonClient.py]
|
||||
AS <-- WebSockets --> CC
|
||||
|
||||
subgraph "Starcraft 2"
|
||||
SC2[Starcraft 2 Game Client]
|
||||
SC2C[Starcraft2Client.py]
|
||||
SC2AI[apsc2 Python Package]
|
||||
|
||||
SC2C <--> SC2AI <-- WebSockets --> SC2
|
||||
end
|
||||
CC <-- Integrated --> SC2C
|
||||
|
||||
%% ChecksFinder
|
||||
subgraph ChecksFinder
|
||||
CFC[ChecksFinderClient]
|
||||
@@ -72,12 +81,14 @@ flowchart LR
|
||||
V6[VVVVVV]
|
||||
MT[Meritous]
|
||||
TW[The Witness]
|
||||
SA2B[Sonic Adventure 2: Battle]
|
||||
|
||||
APCLIENTPP <--> SOE
|
||||
APCLIENTPP <--> MT
|
||||
APCLIENTPP <-- The Witness Randomizer --> TW
|
||||
APCPP <--> SM64
|
||||
APCPP <--> V6
|
||||
APCPP <--> SA2B
|
||||
end
|
||||
SOE <--> SNI <-- Various, depending on SNES device --> SOESNES
|
||||
AS <-- WebSockets --> APCLIENTPP
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@@ -1,4 +1,4 @@
|
||||
colorama>=0.4.4
|
||||
colorama>=0.4.5
|
||||
websockets>=10.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.9.0
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import Utils
|
||||
from Patch import read_rom
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
LTTPJPN10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f'
|
||||
ROM_PLAYER_LIMIT = 255
|
||||
|
||||
@@ -19,7 +19,7 @@ import threading
|
||||
import xxtea
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location
|
||||
from worlds.alttp.Shops import ShopType, ShopPriceType
|
||||
@@ -186,7 +186,7 @@ def check_enemizer(enemizercli):
|
||||
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
|
||||
wanted_version = (7, 0, 1)
|
||||
# version info is saved on the lib, for some reason
|
||||
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
|
||||
with open(library_info) as f:
|
||||
@@ -197,10 +197,11 @@ def check_enemizer(enemizercli):
|
||||
version = lib.split("/")[-1]
|
||||
version = tuple(int(element) for element in version.split("."))
|
||||
enemizer_logger.debug(f"Found Enemizer version {version}")
|
||||
if version < (6, 4, 0):
|
||||
if version < wanted_version:
|
||||
raise Exception(
|
||||
f"Enemizer found at {enemizercli} is outdated ({info}), please update your Enemizer. "
|
||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
|
||||
f"please update your Enemizer. "
|
||||
f"Such as from https://github.com/Ijwu/Enemizer/releases")
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Could not find Enemizer library version information in {library_info}")
|
||||
@@ -1645,8 +1646,7 @@ def patch_rom(world, rom, player, enemized):
|
||||
# set rom name
|
||||
# 21 bytes
|
||||
from Main import __version__
|
||||
# TODO: Adjust Enemizer to accept AP and AD
|
||||
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
rom.name.extend([0] * (21 - len(rom.name)))
|
||||
rom.write_bytes(0x7FC0, rom.name)
|
||||
|
||||
@@ -1677,7 +1677,7 @@ def patch_race_rom(rom, world, player):
|
||||
rom.encrypt(world, player)
|
||||
|
||||
|
||||
def get_price_data(price: int, price_type: int) -> bytes:
|
||||
def get_price_data(price: int, price_type: int) -> List[int]:
|
||||
if price_type != ShopPriceType.Rupees:
|
||||
# Set special price flag 0x8000
|
||||
# Then set the type of price we're setting 0x7F00 (this starts from Hearts, not Rupees, subtract 1)
|
||||
@@ -2890,7 +2890,7 @@ hash_alphabet = [
|
||||
|
||||
|
||||
class LttPDeltaPatch(Patch.APDeltaPatch):
|
||||
hash = JAP10HASH
|
||||
hash = LTTPJPN10HASH
|
||||
game = "A Link to the Past"
|
||||
patch_file_ending = ".aplttp"
|
||||
|
||||
@@ -2907,8 +2907,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
if LTTPJPN10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for Japan(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
@@ -460,10 +460,11 @@ def shuffle_shops(world, items, player: int):
|
||||
f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
|
||||
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
|
||||
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
|
||||
slots = iter(range(2))
|
||||
if bombupgrades:
|
||||
capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades)
|
||||
capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades)
|
||||
if arrowupgrades:
|
||||
capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades)
|
||||
capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades)
|
||||
else:
|
||||
for item in new_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
||||
@@ -144,7 +144,8 @@ Sólo hay que segiur estos pasos una vez.
|
||||
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
|
||||
3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el
|
||||
default) el Puerto de comandos de red.
|
||||

|
||||
|
||||

|
||||
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
|
||||
SFC (bsnes-mercury Performance)".
|
||||
|
||||
|
||||
@@ -501,6 +501,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
item_stack_sizes: Dict[str, int] = items_future.result()
|
||||
non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1}
|
||||
stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items
|
||||
valid_ingredients: Set[str] = stacking_items | fluids
|
||||
|
||||
# cleanup async helpers
|
||||
pool.shutdown()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||
fluids, stacking_items
|
||||
fluids, stacking_items, valid_ingredients
|
||||
from .Shapes import get_shapes
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
|
||||
@@ -231,7 +231,7 @@ class Factorio(World):
|
||||
"""Generate a recipe from pool with time and cost similar to original * factor"""
|
||||
new_ingredients = {}
|
||||
# have to first sort for determinism, while filtering out non-stacking items
|
||||
pool: typing.List[str] = sorted(pool & stacking_items)
|
||||
pool: typing.List[str] = sorted(pool & valid_ingredients)
|
||||
# then sort with random data to shuffle
|
||||
self.world.random.shuffle(pool)
|
||||
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
|
||||
@@ -329,10 +329,8 @@ class Factorio(World):
|
||||
def set_custom_recipes(self):
|
||||
original_rocket_part = recipes["rocket-part"]
|
||||
science_pack_pools = get_science_pack_pools()
|
||||
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
|
||||
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
||||
self.world.random.shuffle(valid_pool)
|
||||
while any([valid_pool[x] in fluids for x in range(3)]):
|
||||
self.world.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x]: 10 for x in range(3)},
|
||||
original_rocket_part.products,
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
{% from "macros.lua" import dict_to_recipe %}
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
|
||||
{
|
||||
production_type = "input",
|
||||
pipe_picture = assembler2pipepictures(),
|
||||
pipe_covers = pipecoverspictures(),
|
||||
base_area = 10,
|
||||
base_level = -1,
|
||||
pipe_connections = {
|
||||
{ type = "input", position = { 0, 5 } },
|
||||
{ type = "input", position = { 0, -5 } },
|
||||
{ type = "input", position = { 5, 0 } },
|
||||
{ type = "input", position = { -5, 0 } }
|
||||
}
|
||||
},
|
||||
{
|
||||
production_type = "input",
|
||||
pipe_picture = assembler2pipepictures(),
|
||||
pipe_covers = pipecoverspictures(),
|
||||
base_area = 10,
|
||||
base_level = -1,
|
||||
pipe_connections = {
|
||||
{ type = "input", position = { -3, 5 } },
|
||||
{ type = "input", position = { -3, -5 } },
|
||||
{ type = "input", position = { 5, -3 } },
|
||||
{ type = "input", position = { -5, -3 } }
|
||||
}
|
||||
},
|
||||
{
|
||||
production_type = "input",
|
||||
pipe_picture = assembler2pipepictures(),
|
||||
pipe_covers = pipecoverspictures(),
|
||||
base_area = 10,
|
||||
base_level = -1,
|
||||
pipe_connections = {
|
||||
{ type = "input", position = { 3, 5 } },
|
||||
{ type = "input", position = { 3, -5 } },
|
||||
{ type = "input", position = { 5, 3 } },
|
||||
{ type = "input", position = { -5, 3 } }
|
||||
}
|
||||
},
|
||||
off_when_no_fluid_recipe = true
|
||||
}
|
||||
|
||||
{%- for recipe_name, recipe in custom_recipes.items() %}
|
||||
data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}"
|
||||
|
||||
@@ -15,6 +15,8 @@ class GenericWeb(WebWorld):
|
||||
commands = Tutorial('Archipelago Server and Client Commands',
|
||||
'A guide detailing the commands available to the user when participating in an Archipelago session.',
|
||||
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
|
||||
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
|
||||
'English', 'mac_en.md','mac/en', ['Bicoloursnake'])
|
||||
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
|
||||
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
|
||||
setup = Tutorial('Multiworld Setup Tutorial',
|
||||
@@ -25,7 +27,7 @@ class GenericWeb(WebWorld):
|
||||
using_website = Tutorial('Archipelago Website User Guide',
|
||||
'A guide to using the Archipelago website to generate multiworlds or host pre-generated multiworlds.',
|
||||
'English', 'using_website_en.md', 'using_website/en', ['alwaysintreble'])
|
||||
tutorials = [setup, using_website, commands, advanced_settings, triggers, plando]
|
||||
tutorials = [setup, using_website, mac, commands, advanced_settings, triggers, plando]
|
||||
|
||||
|
||||
class GenericWorld(World):
|
||||
|
||||
@@ -52,7 +52,7 @@ For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `o
|
||||
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
|
||||
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
|
||||
|
||||
### Root Options
|
||||
## Root Options
|
||||
|
||||
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
|
||||
options or in some cases nested within other nested options. The only options that should exist in root
|
||||
@@ -95,14 +95,14 @@ games you want settings for.
|
||||
more triggers in the triggers guide. Triggers
|
||||
guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en)
|
||||
|
||||
### Game Options
|
||||
## Game Options
|
||||
|
||||
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
|
||||
give a weight to any option it is possible to have one file that can generate a seed for you where you don't know which
|
||||
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
|
||||
settings. If a game can be rolled it **must** have a settings section even if it is empty.
|
||||
|
||||
#### Universal Game Options
|
||||
### Universal Game Options
|
||||
|
||||
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
||||
|
||||
@@ -174,6 +174,8 @@ A Link to the Past:
|
||||
- Moon Pearl
|
||||
start_location_hints:
|
||||
- Spike Cave
|
||||
priority_locations:
|
||||
- Link's House
|
||||
exclude_locations:
|
||||
- Cave 45
|
||||
item_links:
|
||||
@@ -207,9 +209,10 @@ Timespinner:
|
||||
|
||||
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
|
||||
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
|
||||
* `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. The reason for this is becuase each game has a weight of 10 and the toal of all weights is 20.
|
||||
* `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. This is
|
||||
because each game has a weight of 10 and the total of all weights is 20.
|
||||
* `requires` is set to required release version 0.3.2 or higher.
|
||||
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
|
||||
* `accessibility` is set to `none` which will set this seed to beatable only, so some locations and items may be
|
||||
completely inaccessible but the seed will still be completable.
|
||||
* `progression_balancing` is set on, giving it the default value, meaning we will likely receive important items
|
||||
earlier increasing the chance of having things to do.
|
||||
@@ -225,8 +228,8 @@ Timespinner:
|
||||
1 and 7 will be chosen at random, weighted towards a high number.
|
||||
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this
|
||||
example we have:
|
||||
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
||||
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
|
||||
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
||||
* `Bombs (3): 2` gives us 2 packs of 3 bombs or 6 total bombs
|
||||
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use
|
||||
with no cost.
|
||||
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
||||
@@ -234,22 +237,19 @@ Timespinner:
|
||||
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
||||
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
|
||||
multiworld that can be used for no cost.
|
||||
* `priority_locations` forces a progression item to be placed on the `Link's House` location.
|
||||
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
|
||||
* `item_links`
|
||||
* For `A Link to the Past` all players in the `rods` item link group will share their fire and ice rods and the player
|
||||
items will be replaced with single rupees.
|
||||
* For `Timespinner` all players in the `TSAll` item link group will share their entire item pool and the`Twin Pyramid
|
||||
* For `A Link to the Past` all players in the `rods` item link group will share their fire and ice rods and the player
|
||||
items will be replaced with single rupees.
|
||||
items will be replaced with single rupees.
|
||||
* For `Timespinner` all players in the `TSAll` item link group will share their entire item pool and the `Twin Pyramid
|
||||
Key` and `Timespinner Wheel` will be forced among the worlds of those in the group. The `null` replacement item will, instead
|
||||
of forcing a specific chosen item, allow the generator to randomly pick a filler item in place of putting in another one of the linked item.
|
||||
Key` and `Timespinner Wheel` will be forced among the worlds of those in the group. The `null` replacement item will,
|
||||
instead of forcing a specific chosen item, allow the generator to randomly pick a filler item to replace the player items.
|
||||
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
|
||||
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to
|
||||
the `any_world`
|
||||
result.
|
||||
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the
|
||||
`any_world` result. More information on triggers can be found in the [triggers guide](/tutorial/Archipelago/triggers/en).
|
||||
|
||||
### Generating Multiple Worlds
|
||||
## Generating Multiple Worlds
|
||||
|
||||
YAML files can be configured to generate multiple worlds using only one file. This is mostly useful if you are playing an asynchronous multiworld (shortened to async) and are wanting to submit multiple worlds as they can be condensed into one file, removing the need to manage separate files if one chooses to do so.
|
||||
|
||||
@@ -257,7 +257,7 @@ As a precautionary measure, before submitting a multi-game yaml like this one in
|
||||
|
||||
To configure your file to generate multiple worlds, use 3 dashes `---` on an empty line to separate the ending of one world and the beginning of another world.
|
||||
|
||||
#### Example
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
description: Example of generating multiple worlds. World 1 of 3
|
||||
|
||||
32
worlds/generic/docs/mac_en.md
Normal file
32
worlds/generic/docs/mac_en.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Guide to Run Archipelago from Source Code on macOS
|
||||
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
|
||||
## Prerequisite Software
|
||||
Here is a list of software to install and source code to download.
|
||||
1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
|
||||
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
|
||||
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases).
|
||||
5. If you would like to generate Enemized seeds for ALTTP locally (not on the website), you may need the EnemizerCLI from its [Github releases page](https://github.com/Ijwu/Enemizer/releases).
|
||||
6. An Emulator of your choosing for games that need an emulator. For SNES games, I recommend RetroArch, entirely because it was the easiest for me to setup on macOS. It can be downloaded at the [RetroArch downloads page](https://www.retroarch.com/?page=platforms)
|
||||
## Extracting the Archipelago Directory
|
||||
1. Double click on the Archipelago source code zip file to extract the files to an Archipelago directory.
|
||||
2. Move this Archipelago directory out of your downloads directory.
|
||||
3. Open terminal and navigate to your Archipelago directory.
|
||||
## Setting up a Virtual Environment
|
||||
It is generally recommended that you use a virtual environment to run python based software to avoid contamination that can break some software. If Archipelago is the only piece of software you use that runs from python source code however, it is not necessary to use a virtual environment.
|
||||
1. Open terminal and navigate to the Archipelago directory.
|
||||
2. Run the command `python3 -m venv venv` to create a virtual environment. Running this command will create a new directory at the specified path, so make sure that path is clear for a new directory to be created.
|
||||
3. Run the command `source venv/bin/activate` to activate the virtual environment.
|
||||
4. If you want to exit the virtual environment, run the command `deactivate`.
|
||||
## Steps to Run the Clients
|
||||
1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run.
|
||||
2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run.
|
||||
3. Your client should now be running and rom created (where applicable).
|
||||
## Additional Steps for SNES Games
|
||||
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
||||
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
||||
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
|
||||
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.
|
||||
5. If using EnemizerCLI, extract that downloaded directory and rename it to EnemizerCLI.
|
||||
6. Move the EnemizerCLI directory into the Archipelago directory so that Generate.py can take advantage of it.
|
||||
7. Now that SNI, the client, and the emulator are all running, you should be good to go.
|
||||
File diff suppressed because one or more lines are too long
@@ -74,7 +74,7 @@ class Absorber(ast.NodeTransformer):
|
||||
self.truth_values = truth_values
|
||||
self.truth_values |= {"True", "None", "ANY", "ITEMRANDO"}
|
||||
self.false_values = false_values
|
||||
self.false_values |= {"False", "NONE", "RANDOMELEVATORS"}
|
||||
self.false_values |= {"False", "NONE"}
|
||||
|
||||
super(Absorber, self).__init__()
|
||||
|
||||
@@ -203,7 +203,58 @@ logic_folder = os.path.join(resources_source, "Logic")
|
||||
logic_options: typing.Dict[str, str] = hk_loads(os.path.join(data_folder, "logic_settings.json"))
|
||||
for logic_key, logic_value in logic_options.items():
|
||||
logic_options[logic_key] = logic_value.split(".", 1)[-1]
|
||||
del (logic_options["RANDOMELEVATORS"])
|
||||
|
||||
vanilla_cost_data: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "costs.json"))
|
||||
vanilla_location_costs = {
|
||||
key: {
|
||||
value["term"]: int(value["amount"])
|
||||
}
|
||||
for key, value in vanilla_cost_data.items()
|
||||
if value["amount"] > 0 and value["term"] == "GEO"
|
||||
}
|
||||
|
||||
salubra_geo_costs_by_charm_count = {
|
||||
5: 120,
|
||||
10: 500,
|
||||
18: 900,
|
||||
25: 1400,
|
||||
40: 800
|
||||
}
|
||||
|
||||
# Can't extract this data, so supply it ourselves. Source: the wiki
|
||||
vanilla_shop_costs = {
|
||||
('Sly', 'Simple_Key'): [{'GEO': 950}],
|
||||
('Sly', 'Rancid_Egg'): [{'GEO': 60}],
|
||||
('Sly', 'Lumafly_Lantern'): [{'GEO': 1800}],
|
||||
('Sly', 'Gathering_Swarm'): [{'GEO': 300}],
|
||||
('Sly', 'Stalwart_Shell'): [{'GEO': 200}],
|
||||
('Sly', 'Mask_Shard'): [
|
||||
{'GEO': 150},
|
||||
{'GEO': 500},
|
||||
],
|
||||
('Sly', 'Vessel_Fragment'): [{'GEO': 550}],
|
||||
('Sly_(Key)', 'Heavy_Blow'): [{'GEO': 350}],
|
||||
('Sly_(Key)', 'Elegant_Key'): [{'GEO': 800}],
|
||||
('Sly_(Key)', 'Mask_Shard'): [
|
||||
{'GEO': 800},
|
||||
{'GEO': 1500},
|
||||
],
|
||||
('Sly_(Key)', 'Vessel_Fragment'): [{'GEO': 900}],
|
||||
('Sly_(Key)', 'Sprintmaster'): [{'GEO': 400}],
|
||||
|
||||
('Iselda', 'Wayward_Compass'): [{'GEO': 220}],
|
||||
('Iselda', 'Quill'): [{'GEO': 120}],
|
||||
|
||||
('Salubra', 'Lifeblood_Heart'): [{'GEO': 250}],
|
||||
('Salubra', 'Longnail'): [{'GEO': 300}],
|
||||
('Salubra', 'Steady_Body'): [{'GEO': 120}],
|
||||
('Salubra', 'Shaman_Stone'): [{'GEO': 220}],
|
||||
('Salubra', 'Quick_Focus'): [{'GEO': 800}],
|
||||
|
||||
('Leg_Eater', 'Fragile_Heart'): [{'GEO': 350}],
|
||||
('Leg_Eater', 'Fragile_Greed'): [{'GEO': 250}],
|
||||
('Leg_Eater', 'Fragile_Strength'): [{'GEO': 600}],
|
||||
}
|
||||
extra_pool_options: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "pools.json"))
|
||||
pool_options: typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]] = {}
|
||||
for option in extra_pool_options:
|
||||
@@ -213,8 +264,23 @@ for option in extra_pool_options:
|
||||
for pairing in option["Vanilla"]:
|
||||
items.append(pairing["item"])
|
||||
location_name = pairing["location"]
|
||||
if any(cost_entry["term"] == "CHARMS" for cost_entry in pairing.get("costs", [])):
|
||||
location_name += "_(Requires_Charms)"
|
||||
item_costs = pairing.get("costs", [])
|
||||
if item_costs:
|
||||
if any(cost_entry["term"] == "CHARMS" for cost_entry in item_costs):
|
||||
location_name += "_(Requires_Charms)"
|
||||
#vanilla_shop_costs[pairing["location"], pairing["item"]] = \
|
||||
cost = {
|
||||
entry["term"]: int(entry["amount"]) for entry in item_costs
|
||||
}
|
||||
# Rando4 doesn't include vanilla geo costs for Salubra charms, so dirty hardcode here.
|
||||
if 'CHARMS' in cost:
|
||||
geo = salubra_geo_costs_by_charm_count.get(cost['CHARMS'])
|
||||
if geo:
|
||||
cost['GEO'] = geo
|
||||
|
||||
key = (pairing["location"], pairing["item"])
|
||||
vanilla_shop_costs.setdefault(key, []).append(cost)
|
||||
|
||||
locations.append(location_name)
|
||||
if option["Path"]:
|
||||
# basename carries over from prior entry if no Path given
|
||||
@@ -229,6 +295,12 @@ for option in extra_pool_options:
|
||||
pool_options[basename] = items, locations
|
||||
del extra_pool_options
|
||||
|
||||
# reverse all the vanilla shop costs (really, this is just for Salubra).
|
||||
# When we use these later, we pop off the end of the list so this ensures they are still sorted.
|
||||
vanilla_shop_costs = {
|
||||
k: list(reversed(v)) for k, v in vanilla_shop_costs.items()
|
||||
}
|
||||
|
||||
# items
|
||||
items: typing.Dict[str, typing.Dict] = hk_loads(os.path.join(data_folder, "items.json"))
|
||||
logic_items: typing.Set[str] = set()
|
||||
@@ -364,9 +436,15 @@ for event in events:
|
||||
event_rules.update(connectors_rules)
|
||||
connectors_rules = {}
|
||||
|
||||
|
||||
# Apply some final fixes
|
||||
item_effects.update({
|
||||
'Left_Mothwing_Cloak': {'LEFTDASH': 1},
|
||||
'Right_Mothwing_Cloak': {'RIGHTDASH': 1},
|
||||
})
|
||||
names = sorted({"logic_options", "starts", "pool_options", "locations", "multi_locations", "location_to_region_lookup",
|
||||
"event_names", "item_effects", "items", "logic_items", "region_names",
|
||||
"exits", "connectors", "one_ways"})
|
||||
"exits", "connectors", "one_ways", "vanilla_shop_costs", "vanilla_location_costs"})
|
||||
warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
|
||||
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
|
||||
py.write(warning)
|
||||
@@ -385,6 +463,6 @@ rules_template = template_env.get_template("RulesTemplate.pyt")
|
||||
rules = rules_template.render(location_rules=location_rules, one_ways=one_ways, connectors_rules=connectors_rules,
|
||||
event_rules=event_rules)
|
||||
|
||||
with open("Rules.py", "wt") as py:
|
||||
with open("GeneratedRules.py", "wt") as py:
|
||||
py.write(warning)
|
||||
py.write(rules)
|
||||
|
||||
1699
worlds/hk/GeneratedRules.py
Normal file
1699
worlds/hk/GeneratedRules.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import typing
|
||||
from .ExtractedData import logic_options, starts, pool_options
|
||||
from .Rules import cost_terms
|
||||
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
|
||||
from .Charms import vanilla_costs, names as charm_names
|
||||
@@ -11,19 +12,6 @@ else:
|
||||
Random = typing.Any
|
||||
|
||||
|
||||
class Disabled(Toggle):
|
||||
def __init__(self, value: int):
|
||||
super(Disabled, self).__init__(0)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Toggle:
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
return cls(0)
|
||||
|
||||
|
||||
locations = {"option_" + start: i for i, start in enumerate(starts)}
|
||||
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
|
||||
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
|
||||
@@ -36,6 +24,8 @@ option_docstrings = {
|
||||
"randomization.",
|
||||
"RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. "
|
||||
"Also opens their locations for receiving randomized items.",
|
||||
"RandomizeFocus": "Removes the ability to focus and randomizes it into the item pool.",
|
||||
"RandomizeSwim": "Removes the ability to swim in water and randomizes it into the item pool.",
|
||||
"RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for "
|
||||
"randomization. Includes Charms sold in shops.",
|
||||
"RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.",
|
||||
@@ -59,6 +49,8 @@ option_docstrings = {
|
||||
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
|
||||
"pool and open their locations for randomization.",
|
||||
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
||||
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
|
||||
"Mimic Grubs are always placed in your own game.",
|
||||
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
||||
" and buy an item that is randomized into that location as well.",
|
||||
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
||||
@@ -70,6 +62,7 @@ option_docstrings = {
|
||||
"RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item "
|
||||
"pool, and open their locations for randomization. Does not include journal entries "
|
||||
"gained by killing enemies.",
|
||||
"RandomizeNail": "Removes the ability to swing the nail left, right and up, and shuffles these into the item pool.",
|
||||
"RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.",
|
||||
"RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.",
|
||||
"RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for"
|
||||
@@ -110,12 +103,16 @@ default_on = {
|
||||
"RandomizeRelics"
|
||||
}
|
||||
|
||||
# not supported at this time
|
||||
disabled = {
|
||||
"RandomizeFocus",
|
||||
"RandomizeSwim",
|
||||
"RandomizeMimics",
|
||||
"RandomizeNail",
|
||||
shop_to_option = {
|
||||
"Seer": "SeerRewardSlots",
|
||||
"Grubfather": "GrubfatherRewardSlots",
|
||||
"Sly": "SlyShopSlots",
|
||||
"Sly_(Key)": "SlyKeyShopSlots",
|
||||
"Iselda": "IseldaShopSlots",
|
||||
"Salubra": "SalubraShopSlots",
|
||||
"Leg_Eater": "LegEaterShopSlots",
|
||||
"Salubra_(Requires_Charms)": "IseldaShopSlots",
|
||||
"Egg_Shop": "EggShopSlots",
|
||||
}
|
||||
|
||||
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
|
||||
@@ -124,9 +121,6 @@ for option_name, option_data in pool_options.items():
|
||||
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
if option_name in disabled:
|
||||
extra_data["__doc__"] = "Disabled Option. Not implemented."
|
||||
option = type(option_name, (Disabled,), extra_data)
|
||||
if option_name in default_on:
|
||||
option = type(option_name, (DefaultOnToggle,), extra_data)
|
||||
else:
|
||||
@@ -142,13 +136,36 @@ for option_name in logic_options.values():
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
option = type(option_name, (Toggle,), extra_data)
|
||||
if option_name in disabled:
|
||||
extra_data["__doc__"] = "Disabled Option. Not implemented."
|
||||
option = type(option_name, (Disabled,), extra_data)
|
||||
globals()[option.__name__] = option
|
||||
hollow_knight_logic_options[option.__name__] = option
|
||||
|
||||
|
||||
class RandomizeElevatorPass(Toggle):
|
||||
"""Adds an Elevator Pass item to the item pool, which is then required to use the large elevators connecting
|
||||
City of Tears to the Forgotten Crossroads and Resting Grounds."""
|
||||
display_name = "Randomize Elevator Pass"
|
||||
default = False
|
||||
|
||||
|
||||
class SplitMothwingCloak(Toggle):
|
||||
"""Splits the Mothwing Cloak into left- and right-only versions of the item. Randomly adds a second left or
|
||||
right Mothwing cloak item which functions as the upgrade to Shade Cloak."""
|
||||
display_name = "Split Mothwing Cloak"
|
||||
default = False
|
||||
|
||||
|
||||
class SplitMantisClaw(Toggle):
|
||||
"""Splits the Mantis Claw into left- and right-only versions of the item."""
|
||||
display_name = "Split Mantis Claw"
|
||||
default = False
|
||||
|
||||
|
||||
class SplitCrystalHeart(Toggle):
|
||||
"""Splits the Crystal Heart into left- and right-only versions of the item."""
|
||||
display_name = "Split Crystal Heart"
|
||||
default = False
|
||||
|
||||
|
||||
class MinimumGrubPrice(Range):
|
||||
"""The minimum grub price in the range of prices that an item should cost from Grubfather."""
|
||||
display_name = "Minimum Grub Price"
|
||||
@@ -178,7 +195,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
|
||||
|
||||
|
||||
class MinimumEggPrice(Range):
|
||||
"""The minimum rancid egg price in the range of prices that an item should cost from Ijii.
|
||||
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
display_name = "Minimum Egg Price"
|
||||
range_start = 1
|
||||
@@ -187,7 +204,7 @@ class MinimumEggPrice(Range):
|
||||
|
||||
|
||||
class MaximumEggPrice(MinimumEggPrice):
|
||||
"""The maximum rancid egg price in the range of prices that an item should cost from Ijii.
|
||||
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
display_name = "Maximum Egg Price"
|
||||
default = 10
|
||||
@@ -208,6 +225,22 @@ class MaximumCharmPrice(MinimumCharmPrice):
|
||||
default = 20
|
||||
|
||||
|
||||
class MinimumGeoPrice(Range):
|
||||
"""The minimum geo price for items in geo shops."""
|
||||
display_name = "Minimum Geo Price"
|
||||
range_start = 1
|
||||
range_end = 200
|
||||
default = 1
|
||||
|
||||
|
||||
class MaximumGeoPrice(Range):
|
||||
"""The maximum geo price for items in geo shops."""
|
||||
display_name = "Minimum Geo Price"
|
||||
range_start = 1
|
||||
range_end = 2000
|
||||
default = 400
|
||||
|
||||
|
||||
class RandomCharmCosts(SpecialRange):
|
||||
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
|
||||
This value is distributed among all charms in a random fashion.
|
||||
@@ -256,13 +289,91 @@ class PlandoCharmCosts(OptionDict):
|
||||
return charm_costs
|
||||
|
||||
|
||||
class SlyShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Sly Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Sly Shop Slots"
|
||||
default = 8
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SlyKeyShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Sly Shop (requiring Shopkeeper's Key) and a filler item to the item pool."""
|
||||
|
||||
display_name = "Sly Key Shop Slots"
|
||||
default = 6
|
||||
range_end = 16
|
||||
|
||||
|
||||
class IseldaShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Iselda Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Iselda Shop Slots"
|
||||
default = 2
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SalubraShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Salubra Shop, and a filler item to the item pool."""
|
||||
|
||||
display_name = "Salubra Shop Slots"
|
||||
default = 5
|
||||
range_start = 0
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SalubraCharmShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Salubra Shop (requiring Charms), and a filler item to the item pool."""
|
||||
|
||||
display_name = "Salubra Charm Shop Slots"
|
||||
default = 5
|
||||
range_end = 16
|
||||
|
||||
|
||||
class LegEaterShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Leg Eater Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Leg Eater Shop Slots"
|
||||
default = 3
|
||||
range_end = 16
|
||||
|
||||
|
||||
class GrubfatherRewardSlots(Range):
|
||||
"""For each extra slot, add a location to the Grubfather and a filler item to the item pool."""
|
||||
|
||||
display_name = "Grubfather Reward Slots"
|
||||
default = 7
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SeerRewardSlots(Range):
|
||||
"""For each extra slot, add a location to the Seer and a filler item to the item pool."""
|
||||
|
||||
display_name = "Seer Reward Reward Slots"
|
||||
default = 8
|
||||
range_end = 16
|
||||
|
||||
|
||||
class EggShopSlots(Range):
|
||||
"""For each slot, add a location to the Egg Shop and a Geo drop to the item pool."""
|
||||
"""For each slot, add a location to the Egg Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Egg Shop Item Slots"
|
||||
range_end = 16
|
||||
|
||||
|
||||
class ExtraShopSlots(Range):
|
||||
"""For each extra slot, add a location to a randomly chosen shop a filler item to the item pool.
|
||||
|
||||
The Egg Shop will be excluded from this list unless it has at least one item.
|
||||
|
||||
Shops are capped at 16 items each.
|
||||
"""
|
||||
|
||||
display_name = "Additional Shop Slots"
|
||||
default = 0
|
||||
range_end = 9 * 16 # Number of shops x max slots per shop.
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""The goal required of you in order to complete your run in Archipelago."""
|
||||
display_name = "Goal"
|
||||
@@ -288,6 +399,25 @@ class WhitePalace(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class DeathLink(Choice):
|
||||
"""
|
||||
When you die, everyone dies. Of course the reverse is true too.
|
||||
When enabled, choose how incoming deathlinks are handled:
|
||||
vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
|
||||
shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
|
||||
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
alias_true = 1
|
||||
alias_on = 1
|
||||
alias_yes = 1
|
||||
option_shadeless = 1
|
||||
option_vanilla = 2
|
||||
option_shade = 3
|
||||
|
||||
|
||||
class StartingGeo(Range):
|
||||
"""The amount of starting geo you have."""
|
||||
display_name = "Starting Geo"
|
||||
@@ -296,22 +426,70 @@ class StartingGeo(Range):
|
||||
default = 0
|
||||
|
||||
|
||||
class CostSanity(Choice):
|
||||
"""If enabled, most locations with costs (like stag stations) will have randomly determined costs.
|
||||
If set to shopsonly, CostSanity will only apply to shops (including Grubfather, Seer and Egg Shop).
|
||||
If set to notshops, CostSanity will only apply to non-shops (e.g. Stag stations and Cornifer locations)
|
||||
|
||||
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
option_on = 1
|
||||
alias_true = 1
|
||||
alias_yes = 1
|
||||
option_shopsonly = 2
|
||||
option_notshops = 3
|
||||
display_name = "Cost Sanity"
|
||||
|
||||
|
||||
class CostSanityHybridChance(Range):
|
||||
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
|
||||
range_end = 100
|
||||
default = 10
|
||||
|
||||
|
||||
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
|
||||
for term, cost in cost_terms.items():
|
||||
option_name = f"CostSanity{cost.option}Weight"
|
||||
extra_data = {
|
||||
"__module__": __name__, "range_end": 1000,
|
||||
"__doc__": (
|
||||
f"The likelihood of Costsanity choosing a {cost.option} cost."
|
||||
" Chosen as a sum of all weights from other types."
|
||||
),
|
||||
"default": cost.weight
|
||||
}
|
||||
if cost == 'GEO':
|
||||
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
|
||||
|
||||
option = type(option_name, (Range,), extra_data)
|
||||
globals()[option.__name__] = option
|
||||
cost_sanity_weights[option.__name__] = option
|
||||
|
||||
|
||||
hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
**hollow_knight_randomize_options,
|
||||
RandomizeElevatorPass.__name__: RandomizeElevatorPass,
|
||||
**hollow_knight_logic_options,
|
||||
StartLocation.__name__: StartLocation,
|
||||
MinimumGrubPrice.__name__: MinimumGrubPrice,
|
||||
MaximumGrubPrice.__name__: MaximumGrubPrice,
|
||||
MinimumEssencePrice.__name__: MinimumEssencePrice,
|
||||
MaximumEssencePrice.__name__: MaximumEssencePrice,
|
||||
MinimumCharmPrice.__name__: MinimumCharmPrice,
|
||||
MaximumCharmPrice.__name__: MaximumCharmPrice,
|
||||
RandomCharmCosts.__name__: RandomCharmCosts,
|
||||
PlandoCharmCosts.__name__: PlandoCharmCosts,
|
||||
MinimumEggPrice.__name__: MinimumEggPrice,
|
||||
MaximumEggPrice.__name__: MaximumEggPrice,
|
||||
EggShopSlots.__name__: EggShopSlots,
|
||||
Goal.__name__: Goal,
|
||||
WhitePalace.__name__: WhitePalace,
|
||||
StartingGeo.__name__: StartingGeo,
|
||||
**{
|
||||
option.__name__: option
|
||||
for option in (
|
||||
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
|
||||
MinimumGeoPrice, MaximumGeoPrice,
|
||||
MinimumGrubPrice, MaximumGrubPrice,
|
||||
MinimumEssencePrice, MaximumEssencePrice,
|
||||
MinimumCharmPrice, MaximumCharmPrice,
|
||||
RandomCharmCosts, PlandoCharmCosts,
|
||||
MinimumEggPrice, MaximumEggPrice, EggShopSlots,
|
||||
SlyShopSlots, SlyKeyShopSlots, IseldaShopSlots,
|
||||
SalubraShopSlots, SalubraCharmShopSlots,
|
||||
LegEaterShopSlots, GrubfatherRewardSlots,
|
||||
SeerRewardSlots, ExtraShopSlots,
|
||||
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
|
||||
CostSanity, CostSanityHybridChance,
|
||||
)
|
||||
},
|
||||
**cost_sanity_weights
|
||||
}
|
||||
|
||||
1759
worlds/hk/Rules.py
1759
worlds/hk/Rules.py
File diff suppressed because it is too large
Load Diff
@@ -2,16 +2,19 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import typing
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
logger = logging.getLogger("Hollow Knight")
|
||||
|
||||
from .Items import item_table, lookup_type_to_names, item_name_groups
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled, Goal, WhitePalace
|
||||
from .Rules import set_rules, cost_terms
|
||||
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
|
||||
shop_to_option
|
||||
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
|
||||
event_names, item_effects, connectors, one_ways
|
||||
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
|
||||
from .Charms import names as charm_names
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, LocationProgressType, Tutorial, ItemClassification
|
||||
@@ -98,6 +101,25 @@ logicless_options = {
|
||||
"RandomizeLoreTablets", "RandomizeSoulTotems",
|
||||
}
|
||||
|
||||
# Options that affect vanilla starting items
|
||||
randomizable_starting_items: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||
"RandomizeFocus": ("Focus",),
|
||||
"RandomizeSwim": ("Swim",),
|
||||
"RandomizeNail": ('Upslash', 'Leftslash', 'Rightslash')
|
||||
}
|
||||
|
||||
# Shop cost types.
|
||||
shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||
"Egg_Shop": ("RANCIDEGGS",),
|
||||
"Grubfather": ("GRUBS",),
|
||||
"Seer": ("ESSENCE",),
|
||||
"Salubra_(Requires_Charms)": ("CHARMS", "GEO"),
|
||||
"Sly": ("GEO",),
|
||||
"Sly_(Key)": ("GEO",),
|
||||
"Iselda": ("GEO",),
|
||||
"Salubra": ("GEO",),
|
||||
"Leg_Eater": ("GEO",),
|
||||
}
|
||||
|
||||
class HKWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
@@ -127,19 +149,18 @@ class HKWorld(World):
|
||||
item_name_groups = item_name_groups
|
||||
|
||||
ranges: typing.Dict[str, typing.Tuple[int, int]]
|
||||
shops: typing.Dict[str, str] = {
|
||||
"Egg_Shop": "Egg",
|
||||
"Grubfather": "Grub",
|
||||
"Seer": "Essence",
|
||||
"Salubra_(Requires_Charms)": "Charm"
|
||||
}
|
||||
charm_costs: typing.List[int]
|
||||
cached_filler_items = {}
|
||||
data_version = 2
|
||||
|
||||
def __init__(self, world, player):
|
||||
super(HKWorld, self).__init__(world, player)
|
||||
self.created_multi_locations: typing.Dict[str, int] = Counter()
|
||||
self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
|
||||
location: list() for location in multi_locations
|
||||
}
|
||||
self.ranges = {}
|
||||
self.created_shop_items = 0
|
||||
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
|
||||
|
||||
def generate_early(self):
|
||||
world = self.world
|
||||
@@ -147,16 +168,14 @@ class HKWorld(World):
|
||||
self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
|
||||
# world.exclude_locations[self.player].value.update(white_palace_locations)
|
||||
world.local_items[self.player].value.add("Mimic_Grub")
|
||||
for vendor, unit in self.shops.items():
|
||||
mini = getattr(world, f"Minimum{unit}Price")[self.player]
|
||||
maxi = getattr(world, f"Maximum{unit}Price")[self.player]
|
||||
for term, data in cost_terms.items():
|
||||
mini = getattr(world, f"Minimum{data.option}Price")[self.player]
|
||||
maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
|
||||
# if minimum > maximum, set minimum to maximum
|
||||
mini.value = min(mini.value, maxi.value)
|
||||
self.ranges[unit] = mini.value, maxi.value
|
||||
self.ranges[term] = mini.value, maxi.value
|
||||
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
|
||||
True, None, "Event", self.player))
|
||||
for option_name in disabled:
|
||||
getattr(world, option_name)[self.player].value = 0
|
||||
|
||||
def white_palace_exclusions(self):
|
||||
exclusions = set()
|
||||
@@ -199,55 +218,197 @@ class HKWorld(World):
|
||||
menu_region.locations.append(loc)
|
||||
|
||||
def create_items(self):
|
||||
unfilled_locations = 0
|
||||
# Generate item pool and associated locations (paired in HK)
|
||||
pool: typing.List[HKItem] = []
|
||||
geo_replace: typing.Set[str] = set()
|
||||
if self.world.RemoveSpellUpgrades[self.player]:
|
||||
geo_replace.add("Abyss_Shriek")
|
||||
geo_replace.add("Shade_Soul")
|
||||
geo_replace.add("Descending_Dark")
|
||||
|
||||
wp_exclusions = self.white_palace_exclusions()
|
||||
junk_replace: typing.Set[str] = set()
|
||||
if self.world.RemoveSpellUpgrades[self.player]:
|
||||
junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
|
||||
|
||||
randomized_starting_items = set()
|
||||
for attr, items in randomizable_starting_items.items():
|
||||
if getattr(self.world, attr)[self.player]:
|
||||
randomized_starting_items.update(items)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def _add(item_name: str, location_name: str):
|
||||
"""
|
||||
Adds a pairing of an item and location, doing appropriate checks to see if it should be vanilla or not.
|
||||
"""
|
||||
nonlocal unfilled_locations
|
||||
|
||||
vanilla = not randomized
|
||||
excluded = False
|
||||
|
||||
if not vanilla and location_name in wp_exclusions:
|
||||
if location_name == 'King_Fragment':
|
||||
excluded = True
|
||||
else:
|
||||
vanilla = True
|
||||
|
||||
if item_name in junk_replace:
|
||||
item_name = self.get_filler_item_name()
|
||||
|
||||
item = self.create_item(item_name)
|
||||
|
||||
if location_name == "Start":
|
||||
if item_name in randomized_starting_items:
|
||||
pool.append(item)
|
||||
else:
|
||||
self.world.push_precollected(item)
|
||||
return
|
||||
|
||||
if vanilla:
|
||||
location = self.create_vanilla_location(location_name, item)
|
||||
else:
|
||||
pool.append(item)
|
||||
if location_name in multi_locations: # Create shop locations later.
|
||||
return
|
||||
location = self.create_location(location_name)
|
||||
unfilled_locations += 1
|
||||
if excluded:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
for option_key, option in hollow_knight_randomize_options.items():
|
||||
randomized = getattr(self.world, option_key)[self.player]
|
||||
for item_name, location_name in zip(option.items, option.locations):
|
||||
vanilla = not randomized
|
||||
excluded = False
|
||||
if item_name in geo_replace:
|
||||
item_name = "Geo_Rock-Default"
|
||||
item = self.create_item(item_name)
|
||||
if location_name == "Start":
|
||||
self.world.push_precollected(item)
|
||||
if item_name in junk_replace:
|
||||
item_name = self.get_filler_item_name()
|
||||
|
||||
if (item_name == "Crystal_Heart" and self.world.SplitCrystalHeart[self.player]) or \
|
||||
(item_name == "Mothwing_Cloak" and self.world.SplitMothwingCloak[self.player]):
|
||||
_add("Left_" + item_name, location_name)
|
||||
_add("Right_" + item_name, "Split_" + location_name)
|
||||
continue
|
||||
|
||||
location = self.create_location(location_name)
|
||||
if not vanilla and location_name in wp_exclusions:
|
||||
if location_name == 'King_Fragment':
|
||||
excluded = True
|
||||
if item_name == "Mantis_Claw" and self.world.SplitMantisClaw[self.player]:
|
||||
_add("Left_" + item_name, "Left_" + location_name)
|
||||
_add("Right_" + item_name, "Right_" + location_name)
|
||||
continue
|
||||
if item_name == "Shade_Cloak" and self.world.SplitMothwingCloak[self.player]:
|
||||
if self.world.random.randint(0, 1):
|
||||
item_name = "Left_Mothwing_Cloak"
|
||||
else:
|
||||
vanilla = True
|
||||
if excluded:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
if vanilla:
|
||||
location.place_locked_item(item)
|
||||
else:
|
||||
pool.append(item)
|
||||
item_name = "Right_Mothwing_Cloak"
|
||||
|
||||
for i in range(self.world.EggShopSlots[self.player].value):
|
||||
self.create_location("Egg_Shop")
|
||||
pool.append(self.create_item("Geo_Rock-Default"))
|
||||
_add(item_name, location_name)
|
||||
|
||||
if self.world.RandomizeElevatorPass[self.player]:
|
||||
randomized = True
|
||||
_add("Elevator_Pass", "Elevator_Pass")
|
||||
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
for _ in range(len(locations), getattr(self.world, shop_to_option[shop])[self.player].value):
|
||||
loc = self.create_location(shop)
|
||||
unfilled_locations += 1
|
||||
|
||||
# Balance the pool
|
||||
item_count = len(pool)
|
||||
additional_shop_items = max(item_count - unfilled_locations, self.world.ExtraShopSlots[self.player].value)
|
||||
|
||||
# Add additional shop items, as needed.
|
||||
if additional_shop_items > 0:
|
||||
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
|
||||
if not self.world.EggShopSlots[self.player].value: # No eggshop, so don't place items there
|
||||
shops.remove('Egg_Shop')
|
||||
|
||||
for _ in range(additional_shop_items):
|
||||
shop = self.world.random.choice(shops)
|
||||
loc = self.create_location(shop)
|
||||
unfilled_locations += 1
|
||||
if len(self.created_multi_locations[shop]) >= 16:
|
||||
shops.remove(shop)
|
||||
if not shops:
|
||||
break
|
||||
|
||||
# Create filler items, if needed
|
||||
if item_count < unfilled_locations:
|
||||
pool.extend(self.create_item(self.get_filler_item_name()) for _ in range(unfilled_locations - item_count))
|
||||
self.world.itempool += pool
|
||||
self.apply_costsanity()
|
||||
self.sort_shops_by_cost()
|
||||
|
||||
for shopname in self.shops:
|
||||
prices: typing.List[int] = []
|
||||
locations: typing.List[HKLocation] = []
|
||||
for x in range(1, self.created_multi_locations[shopname]+1):
|
||||
loc = self.world.get_location(self.get_multi_location_name(shopname, x), self.player)
|
||||
locations.append(loc)
|
||||
prices.append(loc.cost)
|
||||
prices.sort()
|
||||
for loc, price in zip(locations, prices):
|
||||
loc.cost = price
|
||||
def sort_shops_by_cost(self):
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
randomized_locations = list(loc for loc in locations if not loc.vanilla)
|
||||
prices = sorted(
|
||||
(loc.costs for loc in randomized_locations),
|
||||
key=lambda costs: (len(costs),) + tuple(costs.values())
|
||||
)
|
||||
for loc, costs in zip(randomized_locations, prices):
|
||||
loc.costs = costs
|
||||
|
||||
def apply_costsanity(self):
|
||||
setting = self.world.CostSanity[self.player].value
|
||||
if not setting:
|
||||
return # noop
|
||||
|
||||
def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
|
||||
if all(x == 0 for x in weights.values()):
|
||||
logger.warning(
|
||||
f"All {desc} weights were zero for {self.world.player_name[self.player]}."
|
||||
f" Setting them to one instead."
|
||||
)
|
||||
weights = {k: 1 for k in weights}
|
||||
|
||||
return {k: v for k, v in weights.items() if v}
|
||||
|
||||
random = self.world.random
|
||||
hybrid_chance = getattr(self.world, f"CostSanityHybridChance")[self.player].value
|
||||
weights = {
|
||||
data.term: getattr(self.world, f"CostSanity{data.option}Weight")[self.player].value
|
||||
for data in cost_terms.values()
|
||||
}
|
||||
weights_geoless = dict(weights)
|
||||
del weights_geoless["GEO"]
|
||||
|
||||
weights = _compute_weights(weights, "CostSanity")
|
||||
weights_geoless = _compute_weights(weights_geoless, "Geoless CostSanity")
|
||||
|
||||
if hybrid_chance > 0:
|
||||
if len(weights) == 1:
|
||||
logger.warning(
|
||||
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
|
||||
f" CostSanityHybridChance will not trigger."
|
||||
)
|
||||
if len(weights_geoless) == 1:
|
||||
logger.warning(
|
||||
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
|
||||
f" CostSanityHybridChance will not trigger in geoless locations."
|
||||
)
|
||||
|
||||
for region in self.world.get_regions(self.player):
|
||||
for location in region.locations:
|
||||
if location.vanilla:
|
||||
continue
|
||||
if not location.costs:
|
||||
continue
|
||||
if location.name == "Vessel_Fragment-Basin":
|
||||
continue
|
||||
if setting == CostSanity.option_notshops and location.basename in multi_locations:
|
||||
continue
|
||||
if setting == CostSanity.option_shopsonly and location.basename not in multi_locations:
|
||||
continue
|
||||
if location.basename in {'Grubfather', 'Seer', 'Eggshop'}:
|
||||
our_weights = dict(weights_geoless)
|
||||
else:
|
||||
our_weights = dict(weights)
|
||||
|
||||
rolls = 1
|
||||
if random.randrange(100) < hybrid_chance:
|
||||
rolls = 2
|
||||
|
||||
if rolls > len(our_weights):
|
||||
terms = list(our_weights.keys()) # Can't randomly choose cost types, using all of them.
|
||||
else:
|
||||
terms = []
|
||||
for _ in range(rolls):
|
||||
term = random.choices(list(our_weights.keys()), list(our_weights.values()))[0]
|
||||
del our_weights[term]
|
||||
terms.append(term)
|
||||
|
||||
location.costs = {term: random.randint(*self.ranges[term]) for term in terms}
|
||||
location.sort_costs()
|
||||
|
||||
def set_rules(self):
|
||||
world = self.world
|
||||
@@ -280,12 +441,24 @@ class HKWorld(World):
|
||||
# 32 bit int
|
||||
slot_data["seed"] = self.world.slot_seeds[self.player].randint(-2147483647, 2147483646)
|
||||
|
||||
for shop, unit in self.shops.items():
|
||||
slot_data[f"{unit}_costs"] = {
|
||||
f"{shop}_{i}":
|
||||
self.world.get_location(f"{shop}_{i}", self.player).cost
|
||||
for i in range(1, 1 + self.created_multi_locations[shop])
|
||||
}
|
||||
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
|
||||
if not self.world.CostSanity[self.player]:
|
||||
for shop, terms in shop_cost_types.items():
|
||||
unit = cost_terms[next(iter(terms))].option
|
||||
if unit == "Geo":
|
||||
continue
|
||||
slot_data[f"{unit}_costs"] = {
|
||||
loc.name: next(iter(loc.costs.values()))
|
||||
for loc in self.created_multi_locations[shop]
|
||||
}
|
||||
|
||||
# HKAP 0.1.0 and later cost data.
|
||||
location_costs = {}
|
||||
for region in self.world.get_regions(self.player):
|
||||
for location in region.locations:
|
||||
if location.costs:
|
||||
location_costs[location.name] = location.costs
|
||||
slot_data["location_costs"] = location_costs
|
||||
|
||||
slot_data["notch_costs"] = self.charm_costs
|
||||
|
||||
@@ -295,30 +468,51 @@ class HKWorld(World):
|
||||
item_data = item_table[name]
|
||||
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
|
||||
|
||||
def create_location(self, name: str) -> HKLocation:
|
||||
unit = self.shops.get(name, None)
|
||||
if unit:
|
||||
cost = self.world.random.randint(*self.ranges[unit])
|
||||
else:
|
||||
cost = 0
|
||||
if name in multi_locations:
|
||||
self.created_multi_locations[name] += 1
|
||||
name = self.get_multi_location_name(name, self.created_multi_locations[name])
|
||||
def create_location(self, name: str, vanilla=False) -> HKLocation:
|
||||
costs = None
|
||||
basename = name
|
||||
if name in shop_cost_types:
|
||||
costs = {
|
||||
term: self.world.random.randint(*self.ranges[term])
|
||||
for term in shop_cost_types[name]
|
||||
}
|
||||
elif name in vanilla_location_costs:
|
||||
costs = vanilla_location_costs[name]
|
||||
|
||||
multi = self.created_multi_locations.get(name)
|
||||
|
||||
if multi is not None:
|
||||
i = len(multi) + 1
|
||||
name = f"{name}_{i}"
|
||||
|
||||
region = self.world.get_region("Menu", self.player)
|
||||
loc = HKLocation(self.player, name, self.location_name_to_id[name], region)
|
||||
if unit:
|
||||
loc.unit = unit
|
||||
loc.cost = cost
|
||||
loc = HKLocation(self.player, name,
|
||||
self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
|
||||
basename=basename)
|
||||
|
||||
if multi is not None:
|
||||
multi.append(loc)
|
||||
|
||||
region.locations.append(loc)
|
||||
return loc
|
||||
|
||||
def create_vanilla_location(self, location: str, item: Item):
|
||||
costs = self.vanilla_shop_costs.get((location, item.name))
|
||||
location = self.create_location(location, vanilla=True)
|
||||
location.place_locked_item(item)
|
||||
if costs:
|
||||
location.costs = costs.pop()
|
||||
|
||||
def collect(self, state, item: HKItem) -> bool:
|
||||
change = super(HKWorld, self).collect(state, item)
|
||||
if change:
|
||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||
state.prog_items[effect_name, item.player] += effect_value
|
||||
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
|
||||
state.prog_items.get(('LEFTDASH', item.player), 0):
|
||||
(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
|
||||
([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
|
||||
return change
|
||||
|
||||
def remove(self, state, item: HKItem) -> bool:
|
||||
@@ -348,17 +542,40 @@ class HKWorld(World):
|
||||
name = world.get_player_name(player)
|
||||
spoiler_handle.write(f'\n{name}\n')
|
||||
hk_world: HKWorld = world.worlds[player]
|
||||
for shop_name, unit_name in cls.shops.items():
|
||||
for x in range(1, hk_world.created_multi_locations[shop_name]+1):
|
||||
loc = world.get_location(hk_world.get_multi_location_name(shop_name, x), player)
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost} {unit_name}")
|
||||
|
||||
if world.CostSanity[player].value:
|
||||
for loc in sorted(
|
||||
(
|
||||
loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player)))
|
||||
if loc.costs
|
||||
), key=operator.attrgetter('name')
|
||||
):
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||
else:
|
||||
for shop_name, locations in hk_world.created_multi_locations.items():
|
||||
for loc in locations:
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||
|
||||
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
|
||||
if i is None:
|
||||
i = self.created_multi_locations[base]
|
||||
assert 0 < i < 18, "limited number of multi location IDs reserved."
|
||||
i = len(self.created_multi_locations[base]) + 1
|
||||
assert 1 <= 16, "limited number of multi location IDs reserved."
|
||||
return f"{base}_{i}"
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.player not in self.cached_filler_items:
|
||||
fillers = ["One_Geo", "Soul_Refill"]
|
||||
exclusions = self.white_palace_exclusions()
|
||||
for group in (
|
||||
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
|
||||
'RandomizeRancidEggs'
|
||||
):
|
||||
if getattr(self.world, group):
|
||||
fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in
|
||||
exclusions)
|
||||
self.cached_filler_items[self.player] = fillers
|
||||
return self.world.random.choice(self.cached_filler_items[self.player])
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, location_names=None, exits=None) -> Region:
|
||||
ret = Region(name, RegionType.Generic, name, player)
|
||||
@@ -376,11 +593,34 @@ def create_region(world: MultiWorld, player: int, name: str, location_names=None
|
||||
|
||||
class HKLocation(Location):
|
||||
game: str = "Hollow Knight"
|
||||
cost: int = 0
|
||||
costs: typing.Dict[str, int] = None
|
||||
unit: typing.Optional[str] = None
|
||||
vanilla = False
|
||||
basename: str
|
||||
|
||||
def __init__(self, player: int, name: str, code=None, parent=None):
|
||||
def sort_costs(self):
|
||||
if self.costs is None:
|
||||
return
|
||||
self.costs = {k: self.costs[k] for k in sorted(self.costs.keys(), key=lambda x: cost_terms[x].sort)}
|
||||
|
||||
def __init__(
|
||||
self, player: int, name: str, code=None, parent=None,
|
||||
costs: typing.Dict[str, int] = None, vanilla: bool = False, basename: str = None
|
||||
):
|
||||
self.basename = basename or name
|
||||
super(HKLocation, self).__init__(player, name, code if code else None, parent)
|
||||
self.vanilla = vanilla
|
||||
if costs:
|
||||
self.costs = dict(costs)
|
||||
self.sort_costs()
|
||||
|
||||
def cost_text(self, separator=" and "):
|
||||
if self.costs is None:
|
||||
return None
|
||||
return separator.join(
|
||||
f"{value} {cost_terms[term].singular if value == 1 else cost_terms[term].plural}"
|
||||
for term, value in self.costs.items()
|
||||
)
|
||||
|
||||
|
||||
class HKItem(Item):
|
||||
@@ -393,6 +633,10 @@ class HKItem(Item):
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
elif type == "Charm" and name not in progression_charms:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
elif type in ("Map", "Journal"):
|
||||
classification = ItemClassification.filler
|
||||
elif type in ("Mask", "Ore", "Vessel"):
|
||||
classification = ItemClassification.useful
|
||||
elif advancement:
|
||||
classification = ItemClassification.progression
|
||||
else:
|
||||
|
||||
@@ -1,50 +1,20 @@
|
||||
from ..generic.Rules import set_rule, add_rule
|
||||
# This module is written by Extractor.py, do not edit manually!.
|
||||
from functools import partial
|
||||
|
||||
units = {
|
||||
"Egg": "RANCIDEGGS",
|
||||
"Grub": "GRUBS",
|
||||
"Essence": "ESSENCE",
|
||||
"Charm": "CHARMS",
|
||||
}
|
||||
|
||||
|
||||
def hk_set_rule(hk_world, location: str, rule):
|
||||
count = hk_world.created_multi_locations[location]
|
||||
if count:
|
||||
locations = [f"{location}_{x}" for x in range(1, count+1)]
|
||||
elif (location, hk_world.player) in hk_world.world._location_cache:
|
||||
locations = [location]
|
||||
else:
|
||||
return
|
||||
for location in locations:
|
||||
set_rule(hk_world.world.get_location(location, hk_world.player), rule)
|
||||
|
||||
|
||||
def set_shop_prices(hk_world):
|
||||
def set_generated_rules(hk_world, hk_set_rule):
|
||||
player = hk_world.player
|
||||
for shop, unit in hk_world.shops.items():
|
||||
for i in range(1, 1 + hk_world.created_multi_locations[shop]):
|
||||
loc = hk_world.world.get_location(f"{shop}_{i}", hk_world.player)
|
||||
add_rule(loc, lambda state, unit=units[unit], cost=loc.cost: state.count(unit, player) >= cost)
|
||||
|
||||
|
||||
def set_rules(hk_world):
|
||||
player = hk_world.player
|
||||
world = hk_world.world
|
||||
fn = partial(hk_set_rule, hk_world)
|
||||
|
||||
# Events
|
||||
{% for location, rule_text in event_rules.items() %}
|
||||
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
|
||||
fn("{{location}}", lambda state: {{rule_text}})
|
||||
{%- endfor %}
|
||||
|
||||
# Locations
|
||||
{% for location, rule_text in location_rules.items() %}
|
||||
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
|
||||
fn("{{location}}", lambda state: {{rule_text}})
|
||||
{%- endfor %}
|
||||
|
||||
# Shop prices
|
||||
set_shop_prices(hk_world)
|
||||
|
||||
# Connectors
|
||||
{% for entrance, rule_text in connectors_rules.items() %}
|
||||
rule = lambda state: {{rule_text}}
|
||||
@@ -54,4 +24,4 @@ def set_rules(hk_world):
|
||||
world.get_entrance("{{entrance}}_R", player).access_rule = lambda state, entrance= entrance: \
|
||||
rule(state) and entrance.can_reach(state)
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
@@ -47,16 +47,29 @@ When the console tells you that you have joined the room, you're all set. Congra
|
||||
multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once
|
||||
everyone is ready use the command `/start`.
|
||||
|
||||
## Manual Installation
|
||||
## Non-Windows Installation
|
||||
|
||||
The Minecraft Client will install forge and the mod for other operating systems but Java has to be provided by the
|
||||
user. Head to [minecraft_versions.json on the MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json)
|
||||
to see which java version is required. New installations will default to the topmost "release" version.
|
||||
- Install the matching Amazon Corretto JDK
|
||||
- see [Manual Installation Software Links](#manual-installation-software-links)
|
||||
- or package manager provided by your OS / distribution
|
||||
- Open your `host.yaml` and add the path to your Java below the `minecraft_options` key
|
||||
- ` java: "path/to/java-xx-amazon-corretto/bin/java"`
|
||||
- Run the Minecraft Client and select your .apmc file
|
||||
|
||||
## Full Manual Installation
|
||||
|
||||
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
|
||||
support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
|
||||
Support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
|
||||
the following links are the versions of the software we use.
|
||||
|
||||
### Manual install Software links
|
||||
### Manual Installation Software Links
|
||||
|
||||
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.18.2.html)
|
||||
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/)
|
||||
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
- **DO NOT INSTALL THIS ON YOUR CLIENT**
|
||||
- [Amazon Corretto Java 17 Download Page](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/downloads-list.html)
|
||||
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
|
||||
- pick the matching version and select "Downloads" on the left
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@ lookup_id_to_name = {}
|
||||
for item in location_table:
|
||||
lookup_id_to_name[item["id"]] = item["name"]
|
||||
|
||||
lookup_id_to_name[None] = "Tangaroa Next Frequency"
|
||||
lookup_id_to_name[None] = "Utopia Complete"
|
||||
lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}
|
||||
@@ -37,6 +37,20 @@ class IslandFrequencyLocations(Choice):
|
||||
option_anywhere = 3
|
||||
default = 1
|
||||
|
||||
class IslandGenerationDistance(Choice):
|
||||
"""Sets how far away islands spawn from you when you input their coordinates into the Receiver."""
|
||||
display_name = "Island distance"
|
||||
option_quarter = 2
|
||||
option_half = 4
|
||||
option_vanilla = 8
|
||||
option_double = 16
|
||||
option_quadrouple = 32
|
||||
default = 8
|
||||
|
||||
class ExpensiveResearch(Toggle):
|
||||
"""Makes unlocking items in the Crafting Table consume the researched items."""
|
||||
display_name = "Expensive research"
|
||||
|
||||
class ProgressiveItems(DefaultOnToggle):
|
||||
"""Makes some items, like the Bow and Arrow, progressive rather than raw unlocks."""
|
||||
display_name = "Progressive items"
|
||||
@@ -55,6 +69,8 @@ raft_options = {
|
||||
"maximum_resource_pack_amount": MaximumResourcePackAmount,
|
||||
"duplicate_items": DuplicateItems,
|
||||
"island_frequency_locations": IslandFrequencyLocations,
|
||||
"island_generation_distance": IslandGenerationDistance,
|
||||
"expensive_research": ExpensiveResearch,
|
||||
"progressive_items": ProgressiveItems,
|
||||
"big_island_early_crafting": BigIslandEarlyCrafting,
|
||||
"paddleboard_mode": PaddleboardMode
|
||||
|
||||
@@ -12,6 +12,9 @@ class RaftLogic(LogicMixin):
|
||||
|
||||
def raft_can_smelt_items(self, player):
|
||||
return self.has("Smelter", player)
|
||||
|
||||
def raft_can_find_titanium(self, player):
|
||||
return self.has("Metal detector", player)
|
||||
|
||||
def raft_can_craft_bolt(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.has("Bolt", player)
|
||||
@@ -76,7 +79,7 @@ class RaftLogic(LogicMixin):
|
||||
return self.raft_can_craft_battery(player) and self.raft_can_craft_reciever(player) and self.raft_can_craft_antenna(player)
|
||||
|
||||
def raft_can_drive(self, player): # The player can go wherever they want with the engine
|
||||
return self.raft_can_craft_engine(player) and self.raft_can_craft_steeringWheel(player)
|
||||
return (self.raft_can_craft_engine(player) and self.raft_can_craft_steeringWheel(player)) or self.raft_paddleboard_mode_enabled(player)
|
||||
|
||||
def raft_can_access_radio_tower(self, player):
|
||||
return self.raft_can_navigate(player)
|
||||
@@ -92,24 +95,42 @@ class RaftLogic(LogicMixin):
|
||||
|
||||
def raft_can_access_balboa_island(self, player):
|
||||
return (self.raft_can_complete_vasagatan(player)
|
||||
and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player))
|
||||
and self.raft_can_drive(player)
|
||||
and self.has("Balboa Island Frequency", player))
|
||||
|
||||
def raft_can_complete_balboa_island(self, player):
|
||||
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player) and self.raft_can_fire_bow(player)
|
||||
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player)
|
||||
|
||||
def raft_can_access_caravan_island(self, player):
|
||||
return self.raft_can_complete_balboa_island(player) and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player)) and self.has("Caravan Island Frequency", player)
|
||||
return self.raft_can_complete_balboa_island(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
|
||||
|
||||
def raft_can_complete_caravan_island(self, player):
|
||||
return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_tangaroa(self, player):
|
||||
return self.raft_can_complete_caravan_island(player) and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player)) and self.has("Tangaroa Frequency", player)
|
||||
return self.raft_can_complete_caravan_island(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
|
||||
|
||||
def raft_can_complete_tangaroa(self, player):
|
||||
return self.raft_can_access_tangaroa(player)
|
||||
|
||||
def raft_can_access_varuna_point(self, player):
|
||||
return self.raft_can_complete_tangaroa(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
|
||||
|
||||
def raft_can_complete_varuna_point(self, player):
|
||||
return self.raft_can_access_varuna_point(player)
|
||||
|
||||
def raft_can_access_temperance(self, player):
|
||||
return self.raft_can_complete_varuna_point(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player)
|
||||
|
||||
def raft_can_complete_temperance(self, player):
|
||||
return self.raft_can_access_temperance(player)
|
||||
|
||||
def raft_can_access_utopia(self, player):
|
||||
return self.raft_can_complete_temperance(player) and self.raft_can_drive(player) and self.has("Utopia Frequency", player)
|
||||
|
||||
def raft_can_complete_utopia(self, player):
|
||||
return self.raft_can_access_utopia(player)
|
||||
|
||||
def set_rules(world, player):
|
||||
regionChecks = {
|
||||
"Raft": lambda state: True,
|
||||
@@ -118,7 +139,10 @@ def set_rules(world, player):
|
||||
"Vasagatan": lambda state: state.raft_can_complete_radio_tower(player) and state.raft_can_access_vasagatan(player),
|
||||
"BalboaIsland": lambda state: state.raft_can_complete_vasagatan(player) and state.raft_can_access_balboa_island(player),
|
||||
"CaravanIsland": lambda state: state.raft_can_complete_balboa_island(player) and state.raft_can_access_caravan_island(player),
|
||||
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player)
|
||||
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player),
|
||||
"Varuna Point": lambda state: state.raft_can_complete_tangaroa(player) and state.raft_can_access_varuna_point(player),
|
||||
"Temperance": lambda state: state.raft_can_complete_varuna_point(player) and state.raft_can_access_temperance(player),
|
||||
"Utopia": lambda state: state.raft_can_complete_temperance(player) and state.raft_can_access_utopia(player)
|
||||
}
|
||||
itemChecks = {
|
||||
"Plank": lambda state: True,
|
||||
@@ -143,15 +167,14 @@ def set_rules(world, player):
|
||||
"Hinge": lambda state: state.raft_can_craft_hinge(player),
|
||||
"CircuitBoard": lambda state: state.raft_can_craft_circuitBoard(player),
|
||||
"PlasticBottle_Empty": lambda state: state.raft_can_craft_plasticBottle(player),
|
||||
"Shear": lambda state: state.raft_can_craft_shears(player),
|
||||
"Wool": lambda state: state.raft_can_capture_animals(player) and state.raft_can_craft_shears(player),
|
||||
"HoneyComb": lambda state: state.raft_can_access_balboa_island(player),
|
||||
"Jar_Bee": lambda state: state.raft_can_access_balboa_island(player) and state.raft_can_smelt_items(player),
|
||||
"Dirt": lambda state: state.raft_can_get_dirt(player),
|
||||
"Egg": lambda state: state.raft_can_capture_animals(player),
|
||||
"TitaniumIngot": lambda state: state.raft_can_smelt_items(player) and state.raft_can_find_titanium(player),
|
||||
# Specific items for story island location checks
|
||||
"Machete": lambda state: state.raft_can_craft_machete(player),
|
||||
"BowAndArrow": lambda state: state.raft_can_fire_bow(player),
|
||||
"Zipline tool": lambda state: state.raft_can_craft_ziplineTool(player)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ class RaftWorld(World):
|
||||
location_name_to_id = locations_lookup_name_to_id
|
||||
options = raft_options
|
||||
|
||||
data_version = 1
|
||||
required_client_version = (0, 2, 0)
|
||||
data_version = 2
|
||||
required_client_version = (0, 3, 4)
|
||||
|
||||
def generate_basic(self):
|
||||
minRPSpecified = self.world.minimum_resource_pack_amount[self.player].value
|
||||
@@ -96,6 +96,11 @@ class RaftWorld(World):
|
||||
slot_data = {}
|
||||
return slot_data
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
if self.world.island_frequency_locations[self.player] in [0, 1]:
|
||||
return [loc.item for loc in self.world.get_filled_locations()]
|
||||
return []
|
||||
|
||||
def create_item_replaceAsNecessary(self, name: str) -> Item:
|
||||
isFrequency = "Frequency" in name
|
||||
shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 2)
|
||||
@@ -132,13 +137,19 @@ class RaftWorld(World):
|
||||
self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency")
|
||||
self.setLocationItem("Relay Station quest", "Caravan Island Frequency")
|
||||
self.setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency")
|
||||
self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency")
|
||||
self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency")
|
||||
self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency")
|
||||
elif self.world.island_frequency_locations[self.player] == 1:
|
||||
self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency")
|
||||
self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency")
|
||||
self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency")
|
||||
self.setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency")
|
||||
self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency")
|
||||
self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency")
|
||||
self.setLocationItemFromRegion("Temperance", "Utopia Frequency")
|
||||
# Victory item
|
||||
self.world.get_location("Tangaroa Next Frequency", self.player).place_locked_item(
|
||||
self.world.get_location("Utopia Complete", self.player).place_locked_item(
|
||||
RaftItem("Victory", ItemClassification.progression, None, player=self.player))
|
||||
|
||||
def setLocationItem(self, location: str, itemName: str):
|
||||
@@ -151,6 +162,12 @@ class RaftWorld(World):
|
||||
self.world.itempool.remove(itemToUse)
|
||||
location = random.choice(list(loc for loc in location_table if loc["region"] == region))
|
||||
self.world.get_location(location["name"], self.player).place_locked_item(itemToUse)
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"IslandGenerationDistance": self.world.island_generation_distance[self.player].value,
|
||||
"ExpensiveResearch": self.world.expensive_research[self.player].value
|
||||
}
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
ret = Region(name, RegionType.Generic, name, player)
|
||||
|
||||
@@ -5,10 +5,10 @@ The player settings page for this game is located <a href="../player-settings">h
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
All of the items from the Research Table, as well as all the note/blueprint pickups from story islands, are changed to location checks. Blueprint items themselves are never given. The Research Table recipes will *remove* the researched items for that recipe once learned, meaning many more resources must be put into the Research Table to get all the unlocks from it.
|
||||
All of the items from the Research Table, as well as all the note/blueprint/character pickups from story islands, are changed to location checks. Blueprint items themselves are never given when receiving a blueprint.
|
||||
|
||||
## What is the goal of Raft when randomized?
|
||||
The goal remains the same: To pick up the note that has the frequency for the next unreleased story island from Tangaroa.
|
||||
The goal remains the same: To complete the game by getting to the end of the game and finishing Utopia.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All of the craftable items from the Research Table and Blueprints, as well as frequencies. Since there are more locations in Raft than there are items to receive, Resource Packs with basic earlygame materials and/or duplicate items may be added to the item pool (configurable).
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Install Raft. The currently-supported Raft version is Update 13: The Renovation Update. If you plan on playing Raft mainly with Archipelago, it's recommended to disable Raft auto-updating through Steam, as there is no beta channel to get old builds.
|
||||
1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. If you plan on playing Raft mainly with Archipelago, it's recommended to disable Raft auto-updating through Steam, as there is no beta channel to get old builds.
|
||||
|
||||
2. Install RML.
|
||||
|
||||
|
||||
@@ -7,335 +7,460 @@
|
||||
{
|
||||
"id": 47002,
|
||||
"progression": false,
|
||||
"name": "Leather helmet"
|
||||
"name": "Big backpack"
|
||||
},
|
||||
{
|
||||
"id": 47003,
|
||||
"progression": false,
|
||||
"name": "Leather body armor"
|
||||
"name": "Leather helmet"
|
||||
},
|
||||
{
|
||||
"id": 47004,
|
||||
"progression": false,
|
||||
"name": "Leather greaves"
|
||||
"name": "Leather body armor"
|
||||
},
|
||||
{
|
||||
"id": 47005,
|
||||
"progression": false,
|
||||
"name": "Flippers"
|
||||
"name": "Leather greaves"
|
||||
},
|
||||
{
|
||||
"id": 47006,
|
||||
"progression": false,
|
||||
"name": "Head light"
|
||||
"name": "Flippers"
|
||||
},
|
||||
{
|
||||
"id": 47007,
|
||||
"progression": false,
|
||||
"name": "Oxygen bottle"
|
||||
"name": "Head light"
|
||||
},
|
||||
{
|
||||
"id": 47008,
|
||||
"progression": false,
|
||||
"name": "Advanced head light"
|
||||
},
|
||||
{
|
||||
"id": 47009,
|
||||
"progression": false,
|
||||
"name": "Oxygen bottle"
|
||||
},
|
||||
{
|
||||
"id": 47010,
|
||||
"progression": true,
|
||||
"name": "Zipline tool"
|
||||
},
|
||||
{
|
||||
"id": 47009,
|
||||
"id": 47011,
|
||||
"progression": false,
|
||||
"name": "Electric zipline tool"
|
||||
},
|
||||
{
|
||||
"id": 47012,
|
||||
"progression": true,
|
||||
"name": "Empty bottle"
|
||||
},
|
||||
{
|
||||
"id": 47010,
|
||||
"progression": false,
|
||||
"name": "Clay bowl"
|
||||
},
|
||||
{
|
||||
"id": 47011,
|
||||
"progression": false,
|
||||
"name": "Bucket"
|
||||
},
|
||||
{
|
||||
"id": 47012,
|
||||
"progression": false,
|
||||
"name": "Healing salve"
|
||||
},
|
||||
{
|
||||
"id": 47013,
|
||||
"progression": false,
|
||||
"name": "Good healing salve"
|
||||
"name": "Empty canteen"
|
||||
},
|
||||
{
|
||||
"id": 47014,
|
||||
"progression": false,
|
||||
"name": "Cookingpot"
|
||||
"name": "Bucket"
|
||||
},
|
||||
{
|
||||
"id": 47015,
|
||||
"progression": false,
|
||||
"name": "Advanced grill"
|
||||
"name": "Clay bowl"
|
||||
},
|
||||
{
|
||||
"id": 47016,
|
||||
"progression": false,
|
||||
"name": "Advanced purifier"
|
||||
"name": "Drinking glass"
|
||||
},
|
||||
{
|
||||
"id": 47017,
|
||||
"progression": false,
|
||||
"name": "Electric purifier"
|
||||
"name": "Healing salve"
|
||||
},
|
||||
{
|
||||
"id": 47018,
|
||||
"progression": false,
|
||||
"name": "Medium crop plot"
|
||||
"name": "Good healing salve"
|
||||
},
|
||||
{
|
||||
"id": 47019,
|
||||
"progression": false,
|
||||
"name": "Large crop plot"
|
||||
"name": "Cookingpot"
|
||||
},
|
||||
{
|
||||
"id": 47020,
|
||||
"progression": true,
|
||||
"name": "Grass plot"
|
||||
"progression": false,
|
||||
"name": "Juicer"
|
||||
},
|
||||
{
|
||||
"id": 47021,
|
||||
"progression": false,
|
||||
"name": "Scarecrow"
|
||||
"name": "Advanced grill"
|
||||
},
|
||||
{
|
||||
"id": 47022,
|
||||
"progression": false,
|
||||
"name": "Sprinkler"
|
||||
"name": "Electric grill"
|
||||
},
|
||||
{
|
||||
"id": 47023,
|
||||
"progression": false,
|
||||
"name": "Honey"
|
||||
"name": "Advanced purifier"
|
||||
},
|
||||
{
|
||||
"id": 47024,
|
||||
"progression": true,
|
||||
"name": "Battery"
|
||||
"progression": false,
|
||||
"name": "Electric purifier"
|
||||
},
|
||||
{
|
||||
"id": 47025,
|
||||
"progression": true,
|
||||
"name": "Bolt"
|
||||
"progression": false,
|
||||
"name": "Advanced small crop plot"
|
||||
},
|
||||
{
|
||||
"id": 47026,
|
||||
"progression": true,
|
||||
"name": "Circuit board"
|
||||
"progression": false,
|
||||
"name": "Advanced medium crop plot"
|
||||
},
|
||||
{
|
||||
"id": 47027,
|
||||
"progression": true,
|
||||
"name": "Hinge"
|
||||
"progression": false,
|
||||
"name": "Advanced large crop plot"
|
||||
},
|
||||
{
|
||||
"id": 47028,
|
||||
"progression": false,
|
||||
"name": "Stationary anchor"
|
||||
"progression": true,
|
||||
"name": "Grass plot"
|
||||
},
|
||||
{
|
||||
"id": 47029,
|
||||
"progression": false,
|
||||
"name": "Engine controls"
|
||||
"name": "Medium crop plot"
|
||||
},
|
||||
{
|
||||
"id": 47030,
|
||||
"progression": true,
|
||||
"name": "Engine"
|
||||
"progression": false,
|
||||
"name": "Large crop plot"
|
||||
},
|
||||
{
|
||||
"id": 47031,
|
||||
"progression": true,
|
||||
"name": "Receiver"
|
||||
"progression": false,
|
||||
"name": "Scarecrow"
|
||||
},
|
||||
{
|
||||
"id": 47032,
|
||||
"progression": true,
|
||||
"name": "Antenna"
|
||||
"progression": false,
|
||||
"name": "Advanced Scarecrow"
|
||||
},
|
||||
{
|
||||
"id": 47033,
|
||||
"progression": true,
|
||||
"name": "Steering Wheel"
|
||||
"progression": false,
|
||||
"name": "Sprinkler"
|
||||
},
|
||||
{
|
||||
"id": 47034,
|
||||
"progression": false,
|
||||
"name": "Battery charger"
|
||||
"name": "Honey"
|
||||
},
|
||||
{
|
||||
"id": 47035,
|
||||
"progression": false,
|
||||
"name": "Hammock"
|
||||
"progression": true,
|
||||
"name": "Battery"
|
||||
},
|
||||
{
|
||||
"id": 47036,
|
||||
"progression": false,
|
||||
"name": "Beehive"
|
||||
"name": "Advanced Battery"
|
||||
},
|
||||
{
|
||||
"id": 47037,
|
||||
"progression": false,
|
||||
"name": "Biofuel refiner"
|
||||
"progression": true,
|
||||
"name": "Bolt"
|
||||
},
|
||||
{
|
||||
"id": 47038,
|
||||
"progression": false,
|
||||
"name": "Birds nest"
|
||||
"progression": true,
|
||||
"name": "Circuit board"
|
||||
},
|
||||
{
|
||||
"id": 47039,
|
||||
"progression": true,
|
||||
"name": "Smelter"
|
||||
"name": "Hinge"
|
||||
},
|
||||
{
|
||||
"id": 47040,
|
||||
"progression": false,
|
||||
"name": "Fuel tank"
|
||||
"name": "Stationary anchor"
|
||||
},
|
||||
{
|
||||
"id": 47041,
|
||||
"progression": false,
|
||||
"name": "Water tank"
|
||||
"name": "Advanced stationary anchor"
|
||||
},
|
||||
{
|
||||
"id": 47042,
|
||||
"progression": false,
|
||||
"name": "Simple collection net"
|
||||
"name": "Engine controls"
|
||||
},
|
||||
{
|
||||
"id": 47043,
|
||||
"progression": false,
|
||||
"name": "Fuel pipe"
|
||||
"progression": true,
|
||||
"name": "Engine"
|
||||
},
|
||||
{
|
||||
"id": 47044,
|
||||
"progression": false,
|
||||
"name": "Water pipe"
|
||||
"progression": true,
|
||||
"name": "Receiver"
|
||||
},
|
||||
{
|
||||
"id": 47045,
|
||||
"progression": false,
|
||||
"name": "Storage"
|
||||
"progression": true,
|
||||
"name": "Antenna"
|
||||
},
|
||||
{
|
||||
"id": 47046,
|
||||
"progression": false,
|
||||
"name": "Large Storage"
|
||||
"progression": true,
|
||||
"name": "Steering Wheel"
|
||||
},
|
||||
{
|
||||
"id": 47047,
|
||||
"progression": false,
|
||||
"name": "Trashcan"
|
||||
"name": "Battery charger"
|
||||
},
|
||||
{
|
||||
"id": 47048,
|
||||
"progression": false,
|
||||
"name": "Zipline"
|
||||
"name": "Wind turbine"
|
||||
},
|
||||
{
|
||||
"id": 47049,
|
||||
"progression": false,
|
||||
"name": "Firework"
|
||||
"name": "Hammock"
|
||||
},
|
||||
{
|
||||
"id": 47050,
|
||||
"progression": false,
|
||||
"name": "Metal axe"
|
||||
"name": "Beehive"
|
||||
},
|
||||
{
|
||||
"id": 47051,
|
||||
"progression": false,
|
||||
"name": "Binoculars"
|
||||
"name": "Biofuel refiner"
|
||||
},
|
||||
{
|
||||
"id": 47052,
|
||||
"progression": false,
|
||||
"name": "Metal fishing rod"
|
||||
"name": "Advanced biofuel refiner"
|
||||
},
|
||||
{
|
||||
"id": 47053,
|
||||
"progression": false,
|
||||
"name": "Scrap hook"
|
||||
"progression": true,
|
||||
"name": "Birds nest"
|
||||
},
|
||||
{
|
||||
"id": 47054,
|
||||
"progression": false,
|
||||
"name": "Metal detector"
|
||||
"name": "Simple collection net"
|
||||
},
|
||||
{
|
||||
"id": 47055,
|
||||
"progression": true,
|
||||
"name": "Shear"
|
||||
"progression": false,
|
||||
"name": "Advanced collection net"
|
||||
},
|
||||
{
|
||||
"id": 47056,
|
||||
"progression": true,
|
||||
"name": "Shovel"
|
||||
"name": "Smelter"
|
||||
},
|
||||
{
|
||||
"id": 47057,
|
||||
"progression": false,
|
||||
"name": "Sweep net"
|
||||
"name": "Electric Smelter"
|
||||
},
|
||||
{
|
||||
"id": 47058,
|
||||
"progression": true,
|
||||
"name": "Basic bow"
|
||||
"progression": false,
|
||||
"name": "Fuel tank"
|
||||
},
|
||||
{
|
||||
"id": 47059,
|
||||
"progression": true,
|
||||
"name": "Stone arrow"
|
||||
"progression": false,
|
||||
"name": "Water tank"
|
||||
},
|
||||
{
|
||||
"id": 47060,
|
||||
"progression": false,
|
||||
"name": "Metal arrow"
|
||||
"name": "Fuel pipe"
|
||||
},
|
||||
{
|
||||
"id": 47061,
|
||||
"progression": false,
|
||||
"name": "Metal Spear"
|
||||
"name": "Water pipe"
|
||||
},
|
||||
{
|
||||
"id": 47062,
|
||||
"progression": false,
|
||||
"name": "Recycler"
|
||||
},
|
||||
{
|
||||
"id": 47063,
|
||||
"progression": false,
|
||||
"name": "Storage"
|
||||
},
|
||||
{
|
||||
"id": 47064,
|
||||
"progression": false,
|
||||
"name": "Large Storage"
|
||||
},
|
||||
{
|
||||
"id": 47065,
|
||||
"progression": false,
|
||||
"name": "Trashcan"
|
||||
},
|
||||
{
|
||||
"id": 47066,
|
||||
"progression": false,
|
||||
"name": "Zipline"
|
||||
},
|
||||
{
|
||||
"id": 47067,
|
||||
"progression": false,
|
||||
"name": "Firework"
|
||||
},
|
||||
{
|
||||
"id": 47068,
|
||||
"progression": false,
|
||||
"name": "Metal axe"
|
||||
},
|
||||
{
|
||||
"id": 47069,
|
||||
"progression": false,
|
||||
"name": "Titanium axe"
|
||||
},
|
||||
{
|
||||
"id": 47070,
|
||||
"progression": false,
|
||||
"name": "Binoculars"
|
||||
},
|
||||
{
|
||||
"id": 47071,
|
||||
"progression": false,
|
||||
"name": "Metal fishing rod"
|
||||
},
|
||||
{
|
||||
"id": 47072,
|
||||
"progression": false,
|
||||
"name": "Scrap hook"
|
||||
},
|
||||
{
|
||||
"id": 47073,
|
||||
"progression": false,
|
||||
"name": "Titanium hook"
|
||||
},
|
||||
{
|
||||
"id": 47074,
|
||||
"progression": true,
|
||||
"name": "Metal detector"
|
||||
},
|
||||
{
|
||||
"id": 47075,
|
||||
"progression": true,
|
||||
"name": "Shear"
|
||||
},
|
||||
{
|
||||
"id": 47076,
|
||||
"progression": true,
|
||||
"name": "Shovel"
|
||||
},
|
||||
{
|
||||
"id": 47077,
|
||||
"progression": false,
|
||||
"name": "Sweep net"
|
||||
},
|
||||
{
|
||||
"id": 47078,
|
||||
"progression": false,
|
||||
"name": "Basic bow"
|
||||
},
|
||||
{
|
||||
"id": 47079,
|
||||
"progression": false,
|
||||
"name": "Stone arrow"
|
||||
},
|
||||
{
|
||||
"id": 47080,
|
||||
"progression": false,
|
||||
"name": "Metal arrow"
|
||||
},
|
||||
{
|
||||
"id": 47081,
|
||||
"progression": false,
|
||||
"name": "Titanium arrow"
|
||||
},
|
||||
{
|
||||
"id": 47082,
|
||||
"progression": true,
|
||||
"name": "Metal spear"
|
||||
},
|
||||
{
|
||||
"id": 47083,
|
||||
"progression": true,
|
||||
"name": "Machete"
|
||||
},
|
||||
{
|
||||
"id": 47063,
|
||||
"id": 47084,
|
||||
"progression": false,
|
||||
"name": "Titanium sword"
|
||||
},
|
||||
{
|
||||
"id": 47085,
|
||||
"progression": true,
|
||||
"name": "Net launcher"
|
||||
},
|
||||
{
|
||||
"id": 47064,
|
||||
"id": 47086,
|
||||
"progression": true,
|
||||
"name": "Net canister"
|
||||
},
|
||||
{
|
||||
"id": 47065,
|
||||
"id": 47087,
|
||||
"progression": true,
|
||||
"name": "Vasagatan Frequency"
|
||||
},
|
||||
{
|
||||
"id": 47066,
|
||||
"id": 47088,
|
||||
"progression": true,
|
||||
"name": "Balboa Island Frequency"
|
||||
},
|
||||
{
|
||||
"id": 47067,
|
||||
"id": 47089,
|
||||
"progression": true,
|
||||
"name": "Tangaroa Frequency"
|
||||
},
|
||||
{
|
||||
"id": 47068,
|
||||
"id": 47090,
|
||||
"progression": true,
|
||||
"name": "Varuna Point Frequency"
|
||||
},
|
||||
{
|
||||
"id": 47091,
|
||||
"progression": true,
|
||||
"name": "Temperance Frequency"
|
||||
},
|
||||
{
|
||||
"id": 47092,
|
||||
"progression": true,
|
||||
"name": "Utopia Frequency"
|
||||
},
|
||||
{
|
||||
"id": 47093,
|
||||
"progression": true,
|
||||
"name": "Caravan Island Frequency"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,62 @@
|
||||
{
|
||||
"Healing salve": "progressive-salve",
|
||||
"Good healing salve": "progressive-salve",
|
||||
"Backpack": "progressive-backpack",
|
||||
"Big backpack": "progressive-backpack",
|
||||
"Clay bowl": "progressive-containers",
|
||||
"Drinking glass": "progressive-containers",
|
||||
"Head light": "progressive-headlight",
|
||||
"Advanced head light": "progressive-headlight",
|
||||
"Biofuel refiner": "progressive-biofuel",
|
||||
"Advanced biofuel refiner": "progressive-biofuel",
|
||||
"Empty bottle": "progressive-bottle",
|
||||
"Empty canteen": "progressive-bottle",
|
||||
"Advanced grill": "progressive-grill",
|
||||
"Electric grill": "progressive-grill",
|
||||
"Advanced purifier": "progressive-purifier",
|
||||
"Electric purifier": "progressive-purifier",
|
||||
"Medium crop plot": "progressive-crop-plot",
|
||||
"Large crop plot": "progressive-crop-plot",
|
||||
"Advanced small crop plot": "progressive-crop-plot",
|
||||
"Advanced medium crop plot": "progressive-crop-plot",
|
||||
"Advanced large crop plot": "progressive-crop-plot",
|
||||
"Battery": "progressive-battery",
|
||||
"Battery charger": "progressive-battery",
|
||||
"Advanced Battery": "progressive-battery",
|
||||
"Wind turbine": "progressive-battery",
|
||||
"Stationary anchor": "progressive-anchor",
|
||||
"Advanced stationary anchor": "progressive-anchor",
|
||||
"Engine": "progressive-engine",
|
||||
"Steering Wheel": "progressive-engine",
|
||||
"Engine controls": "progressive-engine",
|
||||
"Scarecrow": "progressive-scarecrow",
|
||||
"Advanced scarecrow": "progressive-scarecrow",
|
||||
"Simple collection net": "progressive-net",
|
||||
"Advanced collection net": "progressive-net",
|
||||
"Storage": "progressive-storage",
|
||||
"Large Storage": "progressive-storage",
|
||||
"Zipline tool": "progressive-zipline",
|
||||
"Zipline": "progressive-zipline",
|
||||
"Electric zipline tool": "progressive-zipline",
|
||||
"Smelter": "progressive-metals",
|
||||
"Metal detector": "progressive-metals",
|
||||
"Electric Smelter": "progressive-metals",
|
||||
"Basic bow": "progressive-bow",
|
||||
"Stone arrow": "progressive-bow",
|
||||
"Metal arrow": "progressive-bow",
|
||||
"Titanium arrow": "progressive-bow",
|
||||
"Metal axe": "progressive-axe",
|
||||
"Titanium axe": "progressive-axe",
|
||||
"Scrap hook": "progressive-hook",
|
||||
"Titanium hook": "progressive-hook",
|
||||
"Metal spear": "progressive-spear",
|
||||
"Machete": "progressive-spear",
|
||||
"Titanium sword": "progressive-spear",
|
||||
"Vasagatan Frequency": "progressive-frequency",
|
||||
"Balboa Island Frequency": "progressive-frequency",
|
||||
"Caravan Island Frequency": "progressive-frequency",
|
||||
"Tangaroa Frequency": "progressive-frequency"
|
||||
"Tangaroa Frequency": "progressive-frequency",
|
||||
"Varuna Point Frequency": "progressive-frequency",
|
||||
"Temperance Frequency": "progressive-frequency",
|
||||
"Utopia Frequency": "progressive-frequency"
|
||||
}
|
||||
@@ -5,5 +5,8 @@
|
||||
"Vasagatan": ["BalboaIsland"],
|
||||
"BalboaIsland": ["CaravanIsland"],
|
||||
"CaravanIsland": ["Tangaroa"],
|
||||
"Tangaroa": []
|
||||
"Tangaroa": ["Varuna Point"],
|
||||
"Varuna Point": ["Temperance"],
|
||||
"Temperance": ["Utopia"],
|
||||
"Utopia": []
|
||||
}
|
||||
@@ -8,5 +8,9 @@
|
||||
"Thatch",
|
||||
"Sand",
|
||||
"Raw_Beet",
|
||||
"Raw_Potato"
|
||||
"Raw_Potato",
|
||||
"MetalOre",
|
||||
"TitaniumOre",
|
||||
"CopperOre",
|
||||
"ExplosiveGoo"
|
||||
]
|
||||
@@ -23,6 +23,16 @@ all necessary dependencies as well.
|
||||
|
||||
Click on the "Start modded" button in the top left in r2modman to start the game with the Archipelago mod installed.
|
||||
|
||||
## Configuring your YAML File
|
||||
### What is a YAML and why do I need one?
|
||||
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
|
||||
about why Archipelago uses YAML files and what they're for.
|
||||
|
||||
### Where do I get a YAML?
|
||||
You can use the [game settings page for Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago
|
||||
website to generate a YAML using a graphical interface.
|
||||
|
||||
|
||||
## Joining an Archipelago Session
|
||||
|
||||
There will be a menu button on the right side of the screen in the character select menu. Click it in order to bring up
|
||||
@@ -37,71 +47,8 @@ The Risk of Rain 2 players send checks by causing items to spawn in-game. That m
|
||||
generally. An item check is only sent out after a certain number of items are picked up. This count is configurable in
|
||||
the player's YAML.
|
||||
|
||||
## YAML Settings
|
||||
|
||||
An example YAML would look like this:
|
||||
|
||||
```yaml
|
||||
description: Ijwu-ror2
|
||||
name: Ijwu
|
||||
|
||||
game:
|
||||
Risk of Rain 2: 1
|
||||
|
||||
Risk of Rain 2:
|
||||
total_locations: 15
|
||||
total_revivals: 4
|
||||
start_with_revive: true
|
||||
item_pickup_step: 1
|
||||
enable_lunar: true
|
||||
item_weights:
|
||||
default: 50
|
||||
new: 0
|
||||
uncommon: 0
|
||||
legendary: 0
|
||||
lunartic: 0
|
||||
chaos: 0
|
||||
no_scraps: 0
|
||||
even: 0
|
||||
scraps_only: 0
|
||||
item_pool_presets: true
|
||||
# custom item weights
|
||||
green_scrap: 16
|
||||
red_scrap: 4
|
||||
yellow_scrap: 1
|
||||
white_scrap: 32
|
||||
common_item: 64
|
||||
uncommon_item: 32
|
||||
legendary_item: 8
|
||||
boss_item: 4
|
||||
lunar_item: 16
|
||||
equipment: 32
|
||||
```
|
||||
|
||||
| Name | Description | Allowed values |
|
||||
| ---- | ----------- | -------------- |
|
||||
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 100 |
|
||||
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
|
||||
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
|
||||
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
|
||||
| enable_lunar | Allows for lunar items to be shuffled into the item pool on behalf of the Risk of Rain player. | true/false |
|
||||
| item_weights | Each option here is a preset item weight that can be used to customize your generate item pool with certain settings. | default, new, uncommon, legendary, lunartic, chaos, no_scraps, even, scraps_only |
|
||||
| item_pool_presets | A simple toggle to determine whether the item_weight presets are used or the custom item pool as defined below | true/false |
|
||||
| custom item weights | Each defined item here is a single item in the pool that will have a weight against the other items when the item pool gets generated. These values can be modified to adjust how frequently certain items appear | 0-100|
|
||||
|
||||
Using the example YAML above: the Risk of Rain 2 player will have 15 total items which they can pick up for other
|
||||
players. (total_locations = 15)
|
||||
|
||||
They will have 15 items waiting for them in the item pool which will be distributed out to the multiworld. (
|
||||
total_locations = 15)
|
||||
|
||||
They will complete a location check every second item. (item_pickup_step = 1)
|
||||
|
||||
They will have 4 of the items which other players can grant them replaced with `Dio's Best Friend`. (total_revivals = 4)
|
||||
|
||||
The player will also start with a `Dio's Best Friend`. (start_with_revive = true)
|
||||
|
||||
The player will have lunar items shuffled into the item pool on their behalf. (enable_lunar = true)
|
||||
|
||||
The player will have the default preset generated item pool with the custom item weights being ignored. (item_weights:
|
||||
default and item_pool_presets: true)
|
||||
## Commands
|
||||
While playing the multiworld you can type `say` then your message to type in the multiworld chat. All other multiworld
|
||||
remote commands list in the [commands guide](/tutorial/Archipelago/commands/en) work as well in the RoR2 chat. You can
|
||||
also optionally connect to the multiworld using the text client, which can be found in the
|
||||
[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
@@ -6,7 +6,7 @@ class ItemData(typing.NamedTuple):
|
||||
code: typing.Optional[int]
|
||||
type: typing.Optional[str]
|
||||
number: typing.Optional[int]
|
||||
classification: ItemClassification = ItemClassification.filler
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
quantity: int = 1
|
||||
|
||||
|
||||
@@ -59,46 +59,46 @@ item_table = {
|
||||
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9),
|
||||
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10),
|
||||
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.useful),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler),
|
||||
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13),
|
||||
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14),
|
||||
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15),
|
||||
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.useful),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.filler),
|
||||
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.useful),
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler),
|
||||
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1),
|
||||
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2),
|
||||
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3),
|
||||
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4),
|
||||
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.useful),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.useful),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler),
|
||||
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8),
|
||||
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.useful),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler),
|
||||
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.useful),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler),
|
||||
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13),
|
||||
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14),
|
||||
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.useful),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler),
|
||||
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.useful),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.useful),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler),
|
||||
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20),
|
||||
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21),
|
||||
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22),
|
||||
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.useful),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.useful),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler),
|
||||
|
||||
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
|
||||
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
|
||||
"Sensor Tower": ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2),
|
||||
|
||||
"War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0),
|
||||
"Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.useful),
|
||||
"Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.filler),
|
||||
"Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2),
|
||||
"Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3),
|
||||
"Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4),
|
||||
@@ -120,12 +120,12 @@ item_table = {
|
||||
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11),
|
||||
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12),
|
||||
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13),
|
||||
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.useful),
|
||||
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
|
||||
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
|
||||
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.useful),
|
||||
"Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.useful),
|
||||
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler),
|
||||
"Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler),
|
||||
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.useful),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.filler),
|
||||
|
||||
"Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression),
|
||||
"Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression),
|
||||
@@ -137,9 +137,9 @@ item_table = {
|
||||
"Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, classification=ItemClassification.progression),
|
||||
"Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, classification=ItemClassification.progression),
|
||||
|
||||
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0),
|
||||
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0),
|
||||
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0),
|
||||
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
|
||||
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
|
||||
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
|
||||
}
|
||||
|
||||
basic_unit: typing.Tuple[str, ...] = (
|
||||
|
||||
@@ -4,12 +4,12 @@ import os
|
||||
import Utils
|
||||
from Patch import read_rom, APDeltaPatch
|
||||
|
||||
JAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
ROM_PLAYER_LIMIT = 65535
|
||||
|
||||
|
||||
class SMDeltaPatch(APDeltaPatch):
|
||||
hash = JAP10HASH
|
||||
hash = SMJUHASH
|
||||
game = "Super Metroid"
|
||||
patch_file_ending = ".apsm"
|
||||
|
||||
@@ -26,8 +26,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
if SMJUHASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for Japan+US release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
@@ -45,7 +45,8 @@ def set_rules(world, player: int, area_connections):
|
||||
add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("DDD: Pole-Jumping for Red Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
|
||||
add_rule(world.get_location("DDD: 100 Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
|
||||
if world.EnableCoinStars[player]:
|
||||
add_rule(world.get_location("DDD: 100 Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
|
||||
add_rule(world.get_location("SL: Into the Igloo", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("RR: Somewhere Over the Rainbow", player), lambda state: state.has("Cannon Unlock RR", player))
|
||||
|
||||
@@ -4,8 +4,8 @@ import os
|
||||
import Utils
|
||||
from Patch import read_rom, APDeltaPatch
|
||||
|
||||
SMJAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
LTTPJAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
LTTPJPN10HASH = '03a63945398191337e896e5771f77173'
|
||||
ROM_PLAYER_LIMIT = 256
|
||||
|
||||
|
||||
@@ -27,16 +27,16 @@ def get_base_rom_bytes() -> bytes:
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(sm_base_rom_bytes)
|
||||
if SMJAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for SM JAP(1.0) release. '
|
||||
if SMJUHASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for SM Japan+US release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
lttp_file_name = get_lttp_base_rom_path()
|
||||
lttp_base_rom_bytes = bytes(read_rom(open(lttp_file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(lttp_base_rom_bytes)
|
||||
if LTTPJAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for LttP JAP(1.0) release. '
|
||||
if LTTPJPN10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for LttP Japan(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
|
||||
get_base_rom_bytes.base_rom_bytes = bytes(combine_smz3_rom(sm_base_rom_bytes, lttp_base_rom_bytes))
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
- snes9x Multitroid
|
||||
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
|
||||
- RetroArch 1.10.3 or newer from: [RetroArch BuildBot Website](https://buildbot.libretro.com/) - nightly builds
|
||||
are required until 1.10.3 is released. Or,
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html), or
|
||||
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
|
||||
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
|
||||
compatible hardware
|
||||
- Your legally obtained Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc` and
|
||||
|
||||
@@ -4,6 +4,7 @@ from Options import Choice
|
||||
class ItemPool(Choice):
|
||||
"""Valuable item pool moves all not progression relevant items to starting inventory and
|
||||
creates random duplicates of important items in their place."""
|
||||
display_name = "Item Pool"
|
||||
option_standard = 0
|
||||
option_valuable = 1
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
logger = logging.getLogger("Subnautica")
|
||||
|
||||
@@ -65,7 +64,7 @@ class SubnauticaWorld(World):
|
||||
for item_name in self.world.random.choices(sorted(advancement_item_names - {"Neptune Launch Platform"}),
|
||||
k=extras):
|
||||
item = self.create_item(item_name)
|
||||
item.advancement = False # as it's an extra, just fast-fill it somewhere
|
||||
item.classification = ItemClassification.filler # as it's an extra, just fast-fill it somewhere
|
||||
pool.append(item)
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
@@ -52,7 +52,7 @@ class TimespinnerLogic(LogicMixin):
|
||||
return self.has_any({'Security Keycard A', 'Security Keycard B', 'Security Keycard C', 'Security Keycard D'}, player)
|
||||
|
||||
def _timespinner_can_break_walls(self, world: MultiWorld, player: int) -> bool:
|
||||
if is_option_enabled(world, player, "FacebookMode"):
|
||||
if is_option_enabled(world, player, "EyeSpy"):
|
||||
return self.has('Oculus Ring', player)
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -19,9 +19,9 @@ class DownloadableItems(DefaultOnToggle):
|
||||
"With the tablet you will be able to download items at terminals"
|
||||
display_name = "Downloadable items"
|
||||
|
||||
class FacebookMode(Toggle):
|
||||
"Requires Oculus Rift(ng) to spot the weakspots in walls and floors"
|
||||
display_name = "Facebook mode"
|
||||
class EyeSpy(Toggle):
|
||||
"Requires Oculus Ring in inventory to be able to break hidden walls."
|
||||
display_name = "Eye Spy"
|
||||
|
||||
class StartWithMeyef(Toggle):
|
||||
"Start with Meyef, ideal for when you want to play multiplayer."
|
||||
@@ -224,6 +224,37 @@ class LootPool(Choice):
|
||||
option_randomized = 1
|
||||
option_empty = 2
|
||||
|
||||
class DropRateCategory(Choice):
|
||||
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
|
||||
Tiered: Based on item rarity/value
|
||||
Vanilla: Based on bestiary slot the item is placed into
|
||||
Random: Assigned a random tier/drop rate
|
||||
Fixed: Set by the 'Fixed Drop Rate' setting
|
||||
"""
|
||||
display_name = "Drop Rate Category"
|
||||
option_tiered = 0
|
||||
option_vanilla = 1
|
||||
option_randomized = 2
|
||||
option_fixed = 3
|
||||
|
||||
class FixedDropRate(Range):
|
||||
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
|
||||
display_name = "Fixed Drop Rate"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 5
|
||||
|
||||
class LootTierDistro(Choice):
|
||||
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
|
||||
Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items
|
||||
Full Random: Any item has an equal chance of being placed in an enemy's drop slot
|
||||
Inverted Weight: Rarest items show up the most frequently, while common items are the rarest
|
||||
"""
|
||||
display_name = "Loot Tier Distrubution"
|
||||
option_default_weight = 0
|
||||
option_full_random = 1
|
||||
option_inverted_weight = 2
|
||||
|
||||
class ShowBestiary(Toggle):
|
||||
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
|
||||
display_name = "Show Bestiary Entries"
|
||||
@@ -238,7 +269,7 @@ timespinner_options: Dict[str, Option] = {
|
||||
#"ProgressiveVerticalMovement": ProgressiveVerticalMovement,
|
||||
#"ProgressiveKeycards": ProgressiveKeycards,
|
||||
"DownloadableItems": DownloadableItems,
|
||||
"FacebookMode": FacebookMode,
|
||||
"EyeSpy": EyeSpy,
|
||||
"StartWithMeyef": StartWithMeyef,
|
||||
"QuickSeed": QuickSeed,
|
||||
"SpecificKeycards": SpecificKeycards,
|
||||
@@ -257,6 +288,9 @@ timespinner_options: Dict[str, Option] = {
|
||||
"ShopWarpShards": ShopWarpShards,
|
||||
"ShopMultiplier": ShopMultiplier,
|
||||
"LootPool": LootPool,
|
||||
"DropRateCategory": DropRateCategory,
|
||||
"FixedDropRate": FixedDropRate,
|
||||
"LootTierDistro": LootTierDistro,
|
||||
"ShowBestiary": ShowBestiary,
|
||||
"ShowDrops": ShowDrops,
|
||||
"DeathLink": DeathLink,
|
||||
|
||||
@@ -228,7 +228,7 @@ def create_item_with_correct_settings(world: MultiWorld, player: int, name: str)
|
||||
|
||||
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
|
||||
item.classification = ItemClassification.filler
|
||||
elif name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"):
|
||||
elif name == 'Oculus Ring' and not is_option_enabled(world, player, "EyeSpy"):
|
||||
item.classification = ItemClassification.filler
|
||||
elif (name == 'Kobo' or name == 'Merchant Crow') and not is_option_enabled(world, player, "GyreArchives"):
|
||||
item.classification = ItemClassification.filler
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/)
|
||||
, [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner)
|
||||
oder [Timespinner (GOG)](https://www.gog.com/game/timespinner) (andere Versionen werden nicht unterstützt)
|
||||
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
- [Timespinner Randomizer](https://github.com/Jarno458/TsRandomizer)
|
||||
|
||||
## Wie funktioniert's?
|
||||
|
||||
@@ -15,7 +15,7 @@ die Randomisierung der Gegenstände zu erlauben
|
||||
## Installationsanweisungen
|
||||
|
||||
1. Die aktuellsten Dateien des Randomizers findest du ganz oben auf dieser
|
||||
Webseite: [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases). Lade dir unter '
|
||||
Webseite: [Timespinner Randomizer Releases](https://github.com/Jarno458/TsRandomizer/releases). Lade dir unter '
|
||||
Assets' die .zip Datei für dein Betriebssystem herunter
|
||||
2. Entpacke die .zip Datei im Ordner, in dem das Spiel Timespinner installiert ist
|
||||
|
||||
@@ -27,7 +27,7 @@ die Randomisierung der Gegenstände zu erlauben
|
||||
|
||||
... im Ordner in dem die Inhalte aus der .zip Datei entpackt wurden
|
||||
|
||||
Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.com/Jarno458/TsRandomizer)
|
||||
|
||||
## An einer Multiworld teilnehmen
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/)
|
||||
, [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner)
|
||||
or [Timespinner (GOG)](https://www.gog.com/game/timespinner) (other versions are not supported)
|
||||
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
- [Timespinner Randomizer](https://github.com/Jarno458/TsRandomizer)
|
||||
|
||||
## General Concept
|
||||
|
||||
@@ -14,11 +14,11 @@ randomization of the items
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you
|
||||
Download latest release on [Timespinner Randomizer Releases](https://github.com/Jarno458/TsRandomizer/releases) you
|
||||
can find the .zip files on the releases page. Download the zip for your current platform. Then extract the zip to the
|
||||
folder where your Timespinner game is installed. Then just run TsRandomizer.exe (on Windows) or
|
||||
TsRandomizer.bin.x86_64 (on Linux) or TsRandomizer.bin.osx (on Mac) instead of Timespinner.exe to start the game in
|
||||
randomized mode. For more info see the [ReadMe](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
randomized mode. For more info see the [ReadMe](https://github.com/Jarno458/TsRandomizer)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ class WitnessWorld(World):
|
||||
"""
|
||||
game = "The Witness"
|
||||
topology_present = False
|
||||
data_version = 2
|
||||
|
||||
static_logic = StaticWitnessLogic()
|
||||
static_locat = StaticWitnessLocations()
|
||||
static_items = StaticWitnessItems()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Required Software
|
||||
|
||||
- [The Witness for 64-bit Windows (e.g. Steam version)](https://store.steampowered.com/app/210970/The_Witness/)
|
||||
- [The Witness Archipelago Randomizer](https://github.com/JarnoWesthof/The-Witness-Randomizer-for-Archipelago/releases)
|
||||
- [The Witness Archipelago Randomizer](https://github.com/Jarno458/The-Witness-Randomizer-for-Archipelago/releases)
|
||||
|
||||
## Optional Software
|
||||
|
||||
@@ -18,7 +18,7 @@ It is recommended to do every single one of these steps when you connect to a wo
|
||||
1. Launch The Witness
|
||||
2. Start a fresh save (unless you have absolutely no other choice)
|
||||
3. Do not move
|
||||
4. Launch [The Witness Archipelago Randomizer](https://github.com/JarnoWesthof/The-Witness-Randomizer-for-Archipelago)
|
||||
4. Launch [The Witness Archipelago Randomizer](https://github.com/Jarno458/The-Witness-Randomizer-for-Archipelago)
|
||||
5. Enter the Archipelago address, slot name and password
|
||||
6. Press "Randomize"
|
||||
7. Wait for the randomization to fully finish before moving in-game
|
||||
|
||||
Reference in New Issue
Block a user