mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 17:43:53 -07:00
Compare commits
83 Commits
factorio_a
...
use_self.o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2524ddc075 | ||
|
|
d545b78803 | ||
|
|
88b1c94eb2 | ||
|
|
7742d5d804 | ||
|
|
d3e148dcc6 | ||
|
|
b5fccde913 | ||
|
|
55e9b0687a | ||
|
|
79e1bf351e | ||
|
|
fcfea9d9aa | ||
|
|
cfc5508f06 | ||
|
|
62cb5f1fc2 | ||
|
|
7e70b16656 | ||
|
|
7b486b3380 | ||
|
|
09cac0a685 | ||
|
|
12c583533d | ||
|
|
c5af28a649 | ||
|
|
bb0a0f2aca | ||
|
|
0d929b81e8 | ||
|
|
8842f5d5c7 | ||
|
|
817197c14d | ||
|
|
c8adadb08b | ||
|
|
a549af8304 | ||
|
|
4979314825 | ||
|
|
f958af4067 | ||
|
|
7dff09dc1a | ||
|
|
c56cbd0474 | ||
|
|
6c4fdc985d | ||
|
|
b500cf600c | ||
|
|
394633558f | ||
|
|
3e3af385fa | ||
|
|
ff556bf4cc | ||
|
|
a3b0476b4b | ||
|
|
0eefe9e936 | ||
|
|
db1d195cb0 | ||
|
|
45fa9a8f9e | ||
|
|
e9317d4031 | ||
|
|
d9d282c925 | ||
|
|
13122ab466 | ||
|
|
e8f96dabe8 | ||
|
|
1a05bad612 | ||
|
|
8142564156 | ||
|
|
e2109dba50 | ||
|
|
3a09677333 | ||
|
|
d3b09bde12 | ||
|
|
01d0c05259 | ||
|
|
19b8624818 | ||
|
|
1312884fa2 | ||
|
|
6cd5abdc11 | ||
|
|
6b0eb7da79 | ||
|
|
b0a09f67f4 | ||
|
|
c3184e7b19 | ||
|
|
3214cef6cf | ||
|
|
f10431779b | ||
|
|
a9a6c72d2c | ||
|
|
9351fb45ca | ||
|
|
abfc2ddfed | ||
|
|
bf801a1efe | ||
|
|
5bd022138b | ||
|
|
69ae12823a | ||
|
|
57001ced0f | ||
|
|
3fa01a41cd | ||
|
|
87252c14aa | ||
|
|
56ac6573f1 | ||
|
|
d8004f82ef | ||
|
|
597f94dc22 | ||
|
|
49e1fd0b79 | ||
|
|
530617c9a7 | ||
|
|
229a263131 | ||
|
|
a861ede8b3 | ||
|
|
b7111eeccc | ||
|
|
39a92e98c6 | ||
|
|
a83bf2f616 | ||
|
|
e8ceb12281 | ||
|
|
6e38126add | ||
|
|
5e5018dd64 | ||
|
|
c7d4c2f63c | ||
|
|
80fed1c6fb | ||
|
|
b9ce2052c5 | ||
|
|
a83501a2a0 | ||
|
|
6c5f8250fb | ||
|
|
39969abd6a | ||
|
|
737686a88d | ||
|
|
ce2f9312ca |
@@ -115,11 +115,12 @@ class AdventureContext(CommonContext):
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "Retrieved":
|
||||
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
|
||||
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
elif cmd == "SetReply":
|
||||
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
|
||||
self.freeincarnates_used = args["value"]
|
||||
|
||||
@@ -252,15 +252,20 @@ class MultiWorld():
|
||||
range(1, self.players + 1)}
|
||||
|
||||
def set_options(self, args: Namespace) -> None:
|
||||
# TODO - remove this section once all worlds use options dataclasses
|
||||
all_keys: Set[str] = {key for player in self.player_ids for key in
|
||||
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
|
||||
for option_key in all_keys:
|
||||
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
|
||||
f"Please use `self.options.{option_key}` instead.")
|
||||
option.update(getattr(args, option_key, {}))
|
||||
setattr(self, option_key, option)
|
||||
|
||||
for player in self.player_ids:
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
self.worlds[player] = world_type(self, player)
|
||||
self.worlds[player].random = self.per_slot_randoms[player]
|
||||
for option_key in world_type.options_dataclass.type_hints:
|
||||
option_values = getattr(args, option_key, {})
|
||||
setattr(self, option_key, option_values)
|
||||
# TODO - remove this loop once all worlds use options dataclasses
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||
for option_key in options_dataclass.type_hints})
|
||||
|
||||
@@ -491,7 +496,7 @@ class MultiWorld():
|
||||
else:
|
||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||
|
||||
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
|
||||
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
|
||||
if starting_state:
|
||||
if self.has_beaten_game(starting_state):
|
||||
return True
|
||||
@@ -504,7 +509,7 @@ class MultiWorld():
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
while prog_locations:
|
||||
sphere = set()
|
||||
sphere: Set[Location] = set()
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
for location in prog_locations:
|
||||
@@ -524,12 +529,19 @@ class MultiWorld():
|
||||
|
||||
return False
|
||||
|
||||
def get_spheres(self):
|
||||
def get_spheres(self) -> Iterator[Set[Location]]:
|
||||
"""
|
||||
yields a set of locations for each logical sphere
|
||||
|
||||
If there are unreachable locations, the last sphere of reachable
|
||||
locations is followed by an empty set, and then a set of all of the
|
||||
unreachable locations.
|
||||
"""
|
||||
state = CollectionState(self)
|
||||
locations = set(self.get_filled_locations())
|
||||
|
||||
while locations:
|
||||
sphere = set()
|
||||
sphere: Set[Location] = set()
|
||||
|
||||
for location in locations:
|
||||
if location.can_reach(state):
|
||||
@@ -639,34 +651,34 @@ class CollectionState():
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
self.stale[player] = False
|
||||
rrp = self.reachable_regions[player]
|
||||
bc = self.blocked_connections[player]
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
queue = deque(self.blocked_connections[player])
|
||||
start = self.multiworld.get_region('Menu', player)
|
||||
start = self.multiworld.get_region("Menu", player)
|
||||
|
||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||
if start not in rrp:
|
||||
rrp.add(start)
|
||||
bc.update(start.exits)
|
||||
if start not in reachable_regions:
|
||||
reachable_regions.add(start)
|
||||
blocked_connections.update(start.exits)
|
||||
queue.extend(start.exits)
|
||||
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in rrp:
|
||||
bc.remove(connection)
|
||||
if new_region in reachable_regions:
|
||||
blocked_connections.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||
rrp.add(new_region)
|
||||
bc.remove(connection)
|
||||
bc.update(new_region.exits)
|
||||
reachable_regions.add(new_region)
|
||||
blocked_connections.remove(connection)
|
||||
blocked_connections.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
|
||||
# Retry connections if the new region can unblock them
|
||||
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
|
||||
if new_entrance in bc and new_entrance not in queue:
|
||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
|
||||
def copy(self) -> CollectionState:
|
||||
|
||||
41
Fill.py
41
Fill.py
@@ -550,7 +550,7 @@ def flood_items(world: MultiWorld) -> None:
|
||||
break
|
||||
|
||||
|
||||
def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
|
||||
# Overall progression balancing algorithm:
|
||||
# Gather up all locations in a sphere.
|
||||
@@ -558,28 +558,28 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
|
||||
# which gives more locations available by this sphere.
|
||||
balanceable_players: typing.Dict[int, float] = {
|
||||
player: world.worlds[player].options.progression_balancing / 100
|
||||
for player in world.player_ids
|
||||
if world.worlds[player].options.progression_balancing > 0
|
||||
player: multiworld.worlds[player].options.progression_balancing / 100
|
||||
for player in multiworld.player_ids
|
||||
if multiworld.worlds[player].options.progression_balancing > 0
|
||||
}
|
||||
if not balanceable_players:
|
||||
logging.info('Skipping multiworld progression balancing.')
|
||||
else:
|
||||
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
|
||||
logging.debug(balanceable_players)
|
||||
state: CollectionState = CollectionState(world)
|
||||
state: CollectionState = CollectionState(multiworld)
|
||||
checked_locations: typing.Set[Location] = set()
|
||||
unchecked_locations: typing.Set[Location] = set(world.get_locations())
|
||||
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
|
||||
|
||||
total_locations_count: typing.Counter[int] = Counter(
|
||||
location.player
|
||||
for location in world.get_locations()
|
||||
for location in multiworld.get_locations()
|
||||
if not location.locked
|
||||
)
|
||||
reachable_locations_count: typing.Dict[int, int] = {
|
||||
player: 0
|
||||
for player in world.player_ids
|
||||
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
|
||||
for player in multiworld.player_ids
|
||||
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
|
||||
}
|
||||
balanceable_players = {
|
||||
player: balanceable_players[player]
|
||||
@@ -658,7 +658,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
balancing_unchecked_locations.remove(location)
|
||||
if not location.locked:
|
||||
balancing_reachables[location.player] += 1
|
||||
if world.has_beaten_game(balancing_state) or all(
|
||||
if multiworld.has_beaten_game(balancing_state) or all(
|
||||
item_percentage(player, reachables) >= threshold_percentages[player]
|
||||
for player, reachables in balancing_reachables.items()
|
||||
if player in threshold_percentages):
|
||||
@@ -675,7 +675,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
locations_to_test = unlocked_locations[player]
|
||||
items_to_test = list(candidate_items[player])
|
||||
items_to_test.sort()
|
||||
world.random.shuffle(items_to_test)
|
||||
multiworld.random.shuffle(items_to_test)
|
||||
while items_to_test:
|
||||
testing = items_to_test.pop()
|
||||
reducing_state = state.copy()
|
||||
@@ -687,8 +687,8 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
|
||||
if world.has_beaten_game(balancing_state):
|
||||
if not world.has_beaten_game(reducing_state):
|
||||
if multiworld.has_beaten_game(balancing_state):
|
||||
if not multiworld.has_beaten_game(reducing_state):
|
||||
items_to_replace.append(testing)
|
||||
else:
|
||||
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
|
||||
@@ -696,33 +696,32 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
if p < threshold_percentages[player]:
|
||||
items_to_replace.append(testing)
|
||||
|
||||
replaced_items = False
|
||||
old_moved_item_count = moved_item_count
|
||||
|
||||
# sort then shuffle to maintain deterministic behaviour,
|
||||
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
|
||||
world.random.shuffle(replacement_locations)
|
||||
multiworld.random.shuffle(replacement_locations)
|
||||
items_to_replace.sort()
|
||||
world.random.shuffle(items_to_replace)
|
||||
multiworld.random.shuffle(items_to_replace)
|
||||
|
||||
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
|
||||
while replacement_locations and items_to_replace:
|
||||
old_location = items_to_replace.pop()
|
||||
for new_location in replacement_locations:
|
||||
for i, new_location in enumerate(replacement_locations):
|
||||
if new_location.can_fill(state, old_location.item, False) and \
|
||||
old_location.can_fill(state, new_location.item, False):
|
||||
replacement_locations.remove(new_location)
|
||||
replacement_locations.pop(i)
|
||||
swap_location_item(old_location, new_location)
|
||||
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||
f"displacing {old_location.item} into {old_location}")
|
||||
moved_item_count += 1
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||
|
||||
if replaced_items:
|
||||
if old_moved_item_count < moved_item_count:
|
||||
logging.debug(f"Moved {moved_item_count} items so far\n")
|
||||
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
|
||||
for location in get_sphere_locations(state, unlocked):
|
||||
@@ -736,7 +735,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
if world.has_beaten_game(state):
|
||||
if multiworld.has_beaten_game(state):
|
||||
break
|
||||
elif not sphere_locations:
|
||||
logging.warning("Progression Balancing ran out of paths.")
|
||||
|
||||
16
Main.py
16
Main.py
@@ -117,6 +117,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
# remove from_pool items also from early items handling, as starting is plenty early.
|
||||
early = world.early_items[player].get(item_name, 0)
|
||||
if early:
|
||||
world.early_items[player][item_name] = max(0, early-count)
|
||||
remaining_count = count-early
|
||||
if remaining_count > 0:
|
||||
local_early = world.early_local_items[player].get(item_name, 0)
|
||||
if local_early:
|
||||
world.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
del local_early
|
||||
del early
|
||||
|
||||
logger.info('Creating World.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
@@ -156,10 +167,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
|
||||
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value for player in world.player_ids):
|
||||
new_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
|
||||
player: getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value.copy()
|
||||
for player in world.player_ids}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
for count in items.values():
|
||||
|
||||
19
Utils.py
19
Utils.py
@@ -779,6 +779,25 @@ def deprecate(message: str):
|
||||
import warnings
|
||||
warnings.warn(message)
|
||||
|
||||
|
||||
class DeprecateDict(dict):
|
||||
log_message: str
|
||||
should_error: bool
|
||||
|
||||
def __init__(self, message, error: bool = False) -> None:
|
||||
self.log_message = message
|
||||
self.should_error = error
|
||||
super().__init__()
|
||||
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
if self.should_error:
|
||||
deprecate(self.log_message)
|
||||
elif __debug__:
|
||||
import warnings
|
||||
warnings.warn(self.log_message)
|
||||
return super().__getitem__(item)
|
||||
|
||||
|
||||
def _extend_freeze_support() -> None:
|
||||
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
|
||||
# upstream issue: https://github.com/python/cpython/issues/76327
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import zipfile
|
||||
import base64
|
||||
from typing import Union, Dict, Set, Tuple
|
||||
@@ -6,13 +7,7 @@ from flask import request, flash, redirect, url_for, render_template
|
||||
from markupsafe import Markup
|
||||
|
||||
from WebHostLib import app
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('.txt', ".yaml", ".zip"))
|
||||
|
||||
from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file
|
||||
|
||||
from Generate import roll_settings, PlandoOptions
|
||||
from Utils import parse_yamls
|
||||
@@ -51,33 +46,41 @@ def mysterycheck():
|
||||
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||
options = {}
|
||||
for uploaded_file in files:
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if uploaded_file.filename == '':
|
||||
return 'No selected file'
|
||||
if banned_file(uploaded_file.filename):
|
||||
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
|
||||
"Your file was deleted.")
|
||||
# If the user does not select file, the browser will still submit an empty string without a file name.
|
||||
elif uploaded_file.filename == "":
|
||||
return "No selected file."
|
||||
elif uploaded_file.filename in options:
|
||||
return f'Conflicting files named {uploaded_file.filename} submitted'
|
||||
elif uploaded_file and allowed_file(uploaded_file.filename):
|
||||
return f"Conflicting files named {uploaded_file.filename} submitted."
|
||||
elif uploaded_file and allowed_options(uploaded_file.filename):
|
||||
if uploaded_file.filename.endswith(".zip"):
|
||||
if not zipfile.is_zipfile(uploaded_file):
|
||||
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."
|
||||
|
||||
with zipfile.ZipFile(uploaded_file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
uploaded_file.seek(0) # offset from is_zipfile check
|
||||
with zipfile.ZipFile(uploaded_file, "r") as zfile:
|
||||
for file in zfile.infolist():
|
||||
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
|
||||
base_filename = os.path.basename(file.filename)
|
||||
|
||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return ("Uploaded data contained a rom file, "
|
||||
"which is likely to contain copyrighted material. "
|
||||
"Your file was deleted.")
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
if base_filename.endswith(".archipelago"):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
elif base_filename.endswith(".zip"):
|
||||
return "Nested .zip files inside a .zip are not supported."
|
||||
elif banned_file(base_filename):
|
||||
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
|
||||
"material. Your file was deleted.")
|
||||
# Ignore dot-files.
|
||||
elif not base_filename.startswith(".") and allowed_options(base_filename):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options[uploaded_file.filename] = uploaded_file.read()
|
||||
|
||||
if not options:
|
||||
return "Did not find a .yaml file to process."
|
||||
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
|
||||
return options
|
||||
|
||||
|
||||
|
||||
@@ -205,6 +205,12 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
|
||||
# ensure auto launch is on the same page in regard to room activity.
|
||||
with db_session:
|
||||
room: Room = Room.get(id=ctx.room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
|
||||
|
||||
logging.info("Shutting down")
|
||||
|
||||
with Locker(room_id):
|
||||
|
||||
@@ -5,5 +5,5 @@ Flask-Caching>=2.1.0
|
||||
Flask-Compress>=1.14
|
||||
Flask-Limiter>=3.5.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.2.2; python_version >= '3.9'
|
||||
bokeh>=3.3.2; python_version >= '3.9'
|
||||
markupsafe>=2.1.3
|
||||
|
||||
@@ -369,7 +369,7 @@ const setPresets = (optionsData, presetName) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'special_range': {
|
||||
case 'named_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
@@ -576,7 +576,7 @@ class GameSettings {
|
||||
option = parseInt(option, 10);
|
||||
|
||||
let optionAcceptable = false;
|
||||
if ((option > setting.min) && (option < setting.max)) {
|
||||
if ((option >= setting.min) && (option <= setting.max)) {
|
||||
optionAcceptable = true;
|
||||
}
|
||||
if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
{% block head %}
|
||||
<title>Multiworld {{ room.id|suuid }}</title>
|
||||
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
|
||||
<meta name="og:site_name" content="Archipelago">
|
||||
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
|
||||
<meta property="og:type" content="website" />
|
||||
{% if room.seed.slots|length < 2 %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% else %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% endif %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{% endif %}
|
||||
{% if world.web.options_page is string %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.settings_page }}">Options Page</a>
|
||||
<a href="{{ world.web.options_page }}">Options Page</a>
|
||||
{% elif world.web.options_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||
|
||||
@@ -19,7 +19,22 @@ from worlds.Files import AutoPatchRegister
|
||||
from . import app
|
||||
from .models import Seed, Room, Slot, GameDataPackage
|
||||
|
||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||
banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
|
||||
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
|
||||
allowed_generation_extensions = (".archipelago", ".zip")
|
||||
|
||||
|
||||
def allowed_options(filename: str) -> bool:
|
||||
return filename.endswith(allowed_options_extensions)
|
||||
|
||||
|
||||
def allowed_generation(filename: str) -> bool:
|
||||
return filename.endswith(allowed_generation_extensions)
|
||||
|
||||
|
||||
def banned_file(filename: str) -> bool:
|
||||
return filename.endswith(banned_extensions)
|
||||
|
||||
|
||||
def process_multidata(compressed_multidata, files={}):
|
||||
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
|
||||
@@ -61,8 +76,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
if not owner:
|
||||
owner = session["_id"]
|
||||
infolist = zfile.infolist()
|
||||
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
|
||||
flash(Markup("Error: Your .zip file only contains .yaml files. "
|
||||
if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
|
||||
flash(Markup("Error: Your .zip file only contains options files. "
|
||||
'Did you mean to <a href="/generate">generate a game</a>?'))
|
||||
return
|
||||
|
||||
@@ -73,7 +88,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
# Load files.
|
||||
for file in infolist:
|
||||
handler = AutoPatchRegister.get_handler(file.filename)
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
if banned_file(file.filename):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
|
||||
@@ -136,35 +151,34 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
|
||||
|
||||
@app.route('/uploads', methods=['GET', 'POST'])
|
||||
@app.route("/uploads", methods=["GET", "POST"])
|
||||
def uploads():
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
if request.method == "POST":
|
||||
# check if the POST request has a file part.
|
||||
if "file" not in request.files:
|
||||
flash("No file part in POST request.")
|
||||
else:
|
||||
file = request.files['file']
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if zipfile.is_zipfile(file):
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
uploaded_file = request.files["file"]
|
||||
# If the user does not select file, the browser will still submit an empty string without a file name.
|
||||
if uploaded_file.filename == "":
|
||||
flash("No selected file.")
|
||||
elif uploaded_file and allowed_generation(uploaded_file.filename):
|
||||
if zipfile.is_zipfile(uploaded_file):
|
||||
with zipfile.ZipFile(uploaded_file, "r") as zfile:
|
||||
try:
|
||||
res = upload_zip_to_db(zfile)
|
||||
except VersionException:
|
||||
flash(f"Could not load multidata. Wrong Version detected.")
|
||||
else:
|
||||
if type(res) == str:
|
||||
if res is str:
|
||||
return res
|
||||
elif res:
|
||||
return redirect(url_for("view_seed", seed=res.id))
|
||||
else:
|
||||
file.seek(0) # offset from is_zipfile check
|
||||
uploaded_file.seek(0) # offset from is_zipfile check
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
multidata = file.read()
|
||||
multidata = uploaded_file.read()
|
||||
slots, multidata = process_multidata(multidata)
|
||||
except Exception as e:
|
||||
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
||||
@@ -182,7 +196,3 @@ def user_content():
|
||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
|
||||
return render_template("userContent.html", rooms=rooms, seeds=seeds)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('.archipelago', ".zip"))
|
||||
|
||||
@@ -13,7 +13,6 @@ from typing import List
|
||||
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from worlds import lookup_any_location_id_to_name
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
@@ -153,7 +152,7 @@ def get_payload(ctx: ZeldaContext):
|
||||
|
||||
|
||||
def reconcile_shops(ctx: ZeldaContext):
|
||||
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
|
||||
checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations]
|
||||
shops = [location for location in checked_location_names if "Shop" in location]
|
||||
left_slots = [shop for shop in shops if "Left" in shop]
|
||||
middle_slots = [shop for shop in shops if "Middle" in shop]
|
||||
@@ -191,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
|
||||
locations_checked = []
|
||||
location = None
|
||||
for location in ctx.missing_locations:
|
||||
location_name = lookup_any_location_id_to_name[location]
|
||||
location_name = ctx.location_names[location]
|
||||
|
||||
if location_name in Locations.overworld_locations and zone == "overworld":
|
||||
status = locations_array[Locations.major_location_offsets[location_name]]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import platform
|
||||
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast
|
||||
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
|
||||
|
||||
# CommonClient import first to trigger ModuleUpdater
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
@@ -10,7 +10,7 @@ from NetUtils import ClientStatus
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
|
||||
import colorama # type: ignore
|
||||
import colorama
|
||||
|
||||
from zilliandomizer.zri.memory import Memory
|
||||
from zilliandomizer.zri import events
|
||||
@@ -45,7 +45,7 @@ class SetRoomCallback(Protocol):
|
||||
|
||||
class ZillionContext(CommonContext):
|
||||
game = "Zillion"
|
||||
command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor
|
||||
command_processor = ZillionCommandProcessor
|
||||
items_handling = 1 # receive items from other players
|
||||
|
||||
known_name: Optional[str]
|
||||
@@ -278,7 +278,7 @@ class ZillionContext(CommonContext):
|
||||
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
|
||||
return
|
||||
keys = cast(Dict[str, Optional[str]], args["keys"])
|
||||
doors_b64 = keys[f"zillion-{self.auth}-doors"]
|
||||
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
|
||||
if doors_b64:
|
||||
logger.info("received door data from server")
|
||||
doors = base64.b64decode(doors_b64)
|
||||
|
||||
@@ -585,7 +585,7 @@ else
|
||||
-- misaligned, so for GB and GBC we explicitly set the callback on
|
||||
-- vblank instead.
|
||||
-- https://github.com/TASEmulators/BizHawk/issues/3711
|
||||
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then
|
||||
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then
|
||||
event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
|
||||
else
|
||||
event.onframeend(tick)
|
||||
|
||||
@@ -7,7 +7,7 @@ Contributions are welcome. We have a few requests for new contributors:
|
||||
|
||||
* **Ensure that critical changes are covered by tests.**
|
||||
It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
|
||||
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/world%20api.md#tests).
|
||||
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md).
|
||||
If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
|
||||
|
||||
* **Do not introduce unit test failures/regressions.**
|
||||
|
||||
90
docs/tests.md
Normal file
90
docs/tests.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Archipelago Unit Testing API
|
||||
|
||||
This document covers some of the generic tests available using Archipelago's unit testing system, as well as some basic
|
||||
steps on how to write your own.
|
||||
|
||||
## Generic Tests
|
||||
|
||||
Some generic tests are run on every World to ensure basic functionality with default options. These basic tests can be
|
||||
found in the [general test directory](/test/general).
|
||||
|
||||
## Defining World Tests
|
||||
|
||||
In order to run tests from your world, you will need to create a `test` package within your world package. This can be
|
||||
done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base
|
||||
for your world tests can be created in this file that you can then import into other modules.
|
||||
|
||||
### WorldTestBase
|
||||
|
||||
In order to test basic functionality of varying options, as well as to test specific edge cases or that certain
|
||||
interactions in the world interact as expected, you will want to use the [WorldTestBase](/test/bases.py). This class
|
||||
comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying
|
||||
options combinations.
|
||||
|
||||
Example `/worlds/<my_game>/test/__init__.py`:
|
||||
|
||||
```python
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MyGameTestBase(WorldTestBase):
|
||||
game = "My Game"
|
||||
```
|
||||
|
||||
The basic tests that WorldTestBase comes with include `test_all_state_can_reach_everything`,
|
||||
`test_empty_state_can_reach_something`, and `test_fill`. These test that with all collected items everything is
|
||||
reachable, with no collected items at least something is reachable, and that a valid multiworld can be completed with
|
||||
all steps being called, respectively.
|
||||
|
||||
### Writing Tests
|
||||
|
||||
#### Using WorldTestBase
|
||||
|
||||
Adding runs for the basic tests for a different option combination is as easy as making a new module in the test
|
||||
package, creating a class that inherits from your game's TestBase, and defining the options in a dict as a field on the
|
||||
class. The new module should be named `test_<something>.py` and have at least one class inheriting from the base, or
|
||||
define its own testing methods. Newly defined test methods should follow standard PEP8 snake_case format and also start
|
||||
with `test_`.
|
||||
|
||||
Example `/worlds/<my_game>/test/test_chest_access.py`:
|
||||
|
||||
```python
|
||||
from . import MyGameTestBase
|
||||
|
||||
|
||||
class TestChestAccess(MyGameTestBase):
|
||||
options = {
|
||||
"difficulty": "easy",
|
||||
"final_boss_hp": 4000,
|
||||
}
|
||||
|
||||
def test_sword_chests(self) -> None:
|
||||
"""Test locations that require a sword"""
|
||||
locations = ["Chest1", "Chest2"]
|
||||
items = [["Sword"]]
|
||||
# This tests that the provided locations aren't accessible without the provided items, but can be accessed once
|
||||
# the items are obtained.
|
||||
# This will also check that any locations not provided don't have the same dependency requirement.
|
||||
# Optionally, passing only_check_listed=True to the method will only check the locations provided.
|
||||
self.assertAccessDependency(locations, items)
|
||||
```
|
||||
|
||||
When tests are run, this class will create a multiworld with a single player having the provided options, and run the
|
||||
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
|
||||
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
|
||||
overridden. For more information on what methods are available to your class, check the
|
||||
[WorldTestBase definition](/test/bases.py#L104).
|
||||
|
||||
#### Alternatives to WorldTestBase
|
||||
|
||||
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
|
||||
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
|
||||
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
|
||||
testing portions of your code that can be tested without relying on a multiworld to be created first.
|
||||
|
||||
## Running Tests
|
||||
|
||||
In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`.
|
||||
If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the
|
||||
working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat
|
||||
the steps for the test directory within your world.
|
||||
@@ -870,7 +870,7 @@ TestBase, and can then define options to test in the class body, and run tests i
|
||||
Example `__init__.py`
|
||||
|
||||
```python
|
||||
from test.test_base import WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MyGameTestBase(WorldTestBase):
|
||||
@@ -879,7 +879,7 @@ class MyGameTestBase(WorldTestBase):
|
||||
|
||||
Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
|
||||
|
||||
Example `testChestAccess.py`
|
||||
Example `test_chest_access.py`
|
||||
```python
|
||||
from . import MyGameTestBase
|
||||
|
||||
@@ -899,3 +899,5 @@ class TestChestAccess(MyGameTestBase):
|
||||
# this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them.
|
||||
self.assertAccessDependency(locations, items)
|
||||
```
|
||||
|
||||
For more information on tests check the [tests doc](tests.md).
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
colorama>=0.4.5
|
||||
websockets>=11.0.3
|
||||
websockets>=12.0
|
||||
PyYAML>=6.0.1
|
||||
jellyfish>=1.0.3
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.2.0
|
||||
kivy>=2.2.1
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.0.0
|
||||
certifi>=2023.11.17
|
||||
cython>=3.0.5
|
||||
cython>=3.0.6
|
||||
cymem>=2.0.8
|
||||
orjson>=3.9.10
|
||||
@@ -1,5 +1,8 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
from Fill import distribute_items_restrictive
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestIDs(unittest.TestCase):
|
||||
@@ -66,3 +69,34 @@ class TestIDs(unittest.TestCase):
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
||||
|
||||
def test_postgen_datapackage(self):
|
||||
"""Generates a solo multiworld and checks that the datapackage is still valid"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
datapackage = world_type.get_data_package_data()
|
||||
for item_group, item_names in datapackage["item_name_groups"].items():
|
||||
self.assertIsInstance(item_group, str,
|
||||
f"item_name_group names should be strings: {item_group}")
|
||||
for item_name in item_names:
|
||||
self.assertIsInstance(item_name, str,
|
||||
f"{item_name}, in group {item_group} is not a string")
|
||||
for loc_group, loc_names in datapackage["location_name_groups"].items():
|
||||
self.assertIsInstance(loc_group, str,
|
||||
f"location_name_group names should be strings: {loc_group}")
|
||||
for loc_name in loc_names:
|
||||
self.assertIsInstance(loc_name, str,
|
||||
f"{loc_name}, in group {loc_group} is not a string")
|
||||
for item_name, item_id in datapackage["item_name_to_id"].items():
|
||||
self.assertIsInstance(item_name, str,
|
||||
f"{item_name} is not a valid item name for item_name_to_id")
|
||||
self.assertIsInstance(item_id, int,
|
||||
f"{item_id} for {item_name} should be an int")
|
||||
for loc_name, loc_id in datapackage["location_name_to_id"].items():
|
||||
self.assertIsInstance(loc_name, str,
|
||||
f"{loc_name} is not a valid item name for location_name_to_id")
|
||||
self.assertIsInstance(loc_id, int,
|
||||
f"{loc_id} for {loc_name} should be an int")
|
||||
|
||||
@@ -77,6 +77,10 @@ class AutoWorldRegister(type):
|
||||
# create missing options_dataclass from legacy option_definitions
|
||||
# TODO - remove this once all worlds use options dataclasses
|
||||
if "options_dataclass" not in dct and "option_definitions" in dct:
|
||||
# TODO - switch to deprecate after a version
|
||||
if __debug__:
|
||||
from warnings import warn
|
||||
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
|
||||
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
|
||||
bases=(PerGameCommonOptions,))
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ async def connect(ctx: BizHawkContext) -> bool:
|
||||
|
||||
for port in ports:
|
||||
try:
|
||||
ctx.streams = await asyncio.open_connection("localhost", port)
|
||||
ctx.streams = await asyncio.open_connection("127.0.0.1", port)
|
||||
ctx.connection_status = ConnectionStatus.TENTATIVE
|
||||
ctx._port = port
|
||||
return True
|
||||
|
||||
@@ -208,19 +208,30 @@ async def _run_game(rom: str):
|
||||
|
||||
if auto_start is True:
|
||||
emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
|
||||
subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
subprocess.Popen(
|
||||
[
|
||||
emuhawk_path,
|
||||
f"--lua={Utils.local_path('data', 'lua', 'connector_bizhawk_generic.lua')}",
|
||||
os.path.realpath(rom),
|
||||
],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
elif isinstance(auto_start, str):
|
||||
import shlex
|
||||
|
||||
subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
subprocess.Popen(
|
||||
[
|
||||
*shlex.split(auto_start),
|
||||
os.path.realpath(rom)
|
||||
],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
|
||||
@@ -2,7 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class FreeincarnateMax(Range):
|
||||
@@ -224,21 +226,20 @@ class StartCastle(Choice):
|
||||
default = option_yellow
|
||||
|
||||
|
||||
adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"dragon_slay_check": DragonSlayCheck,
|
||||
"death_link": DeathLink,
|
||||
"bat_logic": BatLogic,
|
||||
"freeincarnate_max": FreeincarnateMax,
|
||||
"dragon_rando_type": DragonRandoType,
|
||||
"connector_multi_slot": ConnectorMultiSlot,
|
||||
"yorgle_speed": YorgleStartingSpeed,
|
||||
"yorgle_min_speed": YorgleMinimumSpeed,
|
||||
"grundle_speed": GrundleStartingSpeed,
|
||||
"grundle_min_speed": GrundleMinimumSpeed,
|
||||
"rhindle_speed": RhindleStartingSpeed,
|
||||
"rhindle_min_speed": RhindleMinimumSpeed,
|
||||
"difficulty_switch_a": DifficultySwitchA,
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
||||
@dataclass
|
||||
class AdventureOptions(PerGameCommonOptions):
|
||||
dragon_slay_check: DragonSlayCheck
|
||||
death_link: DeathLink
|
||||
bat_logic: BatLogic
|
||||
freeincarnate_max: FreeincarnateMax
|
||||
dragon_rando_type: DragonRandoType
|
||||
connector_multi_slot: ConnectorMultiSlot
|
||||
yorgle_speed: YorgleStartingSpeed
|
||||
yorgle_min_speed: YorgleMinimumSpeed
|
||||
grundle_speed: GrundleStartingSpeed
|
||||
grundle_min_speed: GrundleMinimumSpeed
|
||||
rhindle_speed: RhindleStartingSpeed
|
||||
rhindle_min_speed: RhindleMinimumSpeed
|
||||
difficulty_switch_a: DifficultySwitchA
|
||||
difficulty_switch_b: DifficultySwitchB
|
||||
start_castle: StartCastle
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
|
||||
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
|
||||
from Options import PerGameCommonOptions
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||
@@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
|
||||
connect(world, player, target, source, rule, True)
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
for name, locdata in location_table.items():
|
||||
locdata.get_position(multiworld.random)
|
||||
|
||||
@@ -76,7 +77,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
|
||||
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
|
||||
multiworld.regions.append(credits_room_far_side)
|
||||
|
||||
dragon_slay_check = multiworld.dragon_slay_check[player].value
|
||||
dragon_slay_check = options.dragon_slay_check.value
|
||||
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
|
||||
|
||||
for name, location_data in location_table.items():
|
||||
|
||||
@@ -6,7 +6,8 @@ from BaseClasses import LocationProgressType
|
||||
|
||||
def set_rules(self) -> None:
|
||||
world = self.multiworld
|
||||
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
|
||||
options = self.options
|
||||
use_bat_logic = options.bat_logic.value == BatLogic.option_use_logic
|
||||
|
||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
||||
lambda state: state.has("Yellow Key", self.player))
|
||||
@@ -28,7 +29,7 @@ def set_rules(self) -> None:
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
|
||||
dragon_slay_check = world.dragon_slay_check[self.player].value
|
||||
dragon_slay_check = options.dragon_slay_check.value
|
||||
if dragon_slay_check:
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
|
||||
@@ -15,7 +15,7 @@ from Options import AssembleOptions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Options import AdventureOptions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
|
||||
AdventureAutoCollectLocation
|
||||
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
|
||||
@@ -109,7 +109,8 @@ class AdventureWorld(World):
|
||||
game: ClassVar[str] = "Adventure"
|
||||
web: ClassVar[WebWorld] = AdventureWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
||||
options = AdventureOptions
|
||||
options_dataclass = AdventureOptions
|
||||
settings: ClassVar[AdventureSettings]
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
@@ -150,18 +151,18 @@ class AdventureWorld(World):
|
||||
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
|
||||
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
|
||||
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
|
||||
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
|
||||
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
|
||||
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
|
||||
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
|
||||
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
|
||||
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
|
||||
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
|
||||
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
|
||||
self.start_castle = self.multiworld.start_castle[self.player].value
|
||||
self.dragon_rando_type = self.options.dragon_rando_type.value
|
||||
self.dragon_slay_check = self.options.dragon_slay_check.value
|
||||
self.connector_multi_slot = self.options.connector_multi_slot.value
|
||||
self.yorgle_speed = self.options.yorgle_speed.value
|
||||
self.yorgle_min_speed = self.options.yorgle_min_speed.value
|
||||
self.grundle_speed = self.options.grundle_speed.value
|
||||
self.grundle_min_speed = self.options.grundle_min_speed.value
|
||||
self.rhindle_speed = self.options.rhindle_speed.value
|
||||
self.rhindle_min_speed = self.options.rhindle_min_speed.value
|
||||
self.difficulty_switch_a = self.options.difficulty_switch_a.value
|
||||
self.difficulty_switch_b = self.options.difficulty_switch_b.value
|
||||
self.start_castle = self.options.start_castle.value
|
||||
self.created_items = 0
|
||||
|
||||
if self.dragon_slay_check == 0:
|
||||
@@ -228,7 +229,7 @@ class AdventureWorld(World):
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
# traps would probably go here, if enabled
|
||||
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
|
||||
freeincarnate_max = self.options.freeincarnate_max.value
|
||||
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
|
||||
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
|
||||
self.created_items += actual_freeincarnates
|
||||
@@ -248,7 +249,7 @@ class AdventureWorld(World):
|
||||
self.created_items += 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.multiworld, self.player, self.dragon_rooms)
|
||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
@@ -355,7 +356,7 @@ class AdventureWorld(World):
|
||||
auto_collect_locations: [AdventureAutoCollectLocation] = []
|
||||
local_item_to_location: {int, int} = {}
|
||||
bat_no_touch_locs: [LocationData] = []
|
||||
bat_logic: int = self.multiworld.bat_logic[self.player].value
|
||||
bat_logic: int = self.options.bat_logic.value
|
||||
try:
|
||||
rom_deltas: { int, int } = {}
|
||||
self.place_dragons(rom_deltas)
|
||||
@@ -411,7 +412,7 @@ class AdventureWorld(World):
|
||||
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
|
||||
rom_deltas[item_position_data_start] = 0xff
|
||||
|
||||
if self.multiworld.connector_multi_slot[self.player].value:
|
||||
if self.options.connector_multi_slot.value:
|
||||
rom_deltas[connector_port_offset] = (self.player & 0xff)
|
||||
else:
|
||||
rom_deltas[connector_port_offset] = 0
|
||||
|
||||
@@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
|
||||
if loc in all_state_base.events:
|
||||
all_state_base.events.remove(loc)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
|
||||
name="LttP Dungeon Items")
|
||||
|
||||
|
||||
|
||||
@@ -682,8 +682,6 @@ def get_pool_core(world, player: int):
|
||||
key_location = world.random.choice(key_locations)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
pool = pool[:-3]
|
||||
if world.key_drop_shuffle[player]:
|
||||
pass # pool.extend([item_to_place] * (len(key_drop_data) - 1))
|
||||
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
|
||||
additional_pieces_to_place)
|
||||
|
||||
@@ -136,7 +136,8 @@ def mirrorless_path_to_castle_courtyard(world, player):
|
||||
|
||||
def set_defeat_dungeon_boss_rule(location):
|
||||
# Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used
|
||||
set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
|
||||
add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
|
||||
|
||||
|
||||
def set_always_allow(spot, rule):
|
||||
spot.always_allow = rule
|
||||
|
||||
@@ -289,12 +289,17 @@ class ALTTPWorld(World):
|
||||
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
|
||||
if multiworld.mode[player] == 'standard' \
|
||||
and multiworld.smallkey_shuffle[player] \
|
||||
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \
|
||||
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \
|
||||
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with:
|
||||
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
||||
if multiworld.mode[player] == 'standard':
|
||||
if multiworld.smallkey_shuffle[player]:
|
||||
if (multiworld.smallkey_shuffle[player] not in
|
||||
(smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons,
|
||||
smallkey_shuffle.option_start_with)):
|
||||
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
||||
self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)")
|
||||
self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)")
|
||||
if multiworld.bigkey_shuffle[player]:
|
||||
self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)")
|
||||
self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)")
|
||||
|
||||
# system for sharing ER layouts
|
||||
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
|
||||
|
||||
@@ -163,26 +164,26 @@ class BlasphemousDeathLink(DeathLink):
|
||||
Note that Guilt Fragments will not appear when killed by Death Link."""
|
||||
|
||||
|
||||
blasphemous_options = {
|
||||
"prie_dieu_warp": PrieDieuWarp,
|
||||
"skip_cutscenes": SkipCutscenes,
|
||||
"corpse_hints": CorpseHints,
|
||||
"difficulty": Difficulty,
|
||||
"penitence": Penitence,
|
||||
"starting_location": StartingLocation,
|
||||
"ending": Ending,
|
||||
"skip_long_quests": SkipLongQuests,
|
||||
"thorn_shuffle" : ThornShuffle,
|
||||
"dash_shuffle": DashShuffle,
|
||||
"wall_climb_shuffle": WallClimbShuffle,
|
||||
"reliquary_shuffle": ReliquaryShuffle,
|
||||
"boots_of_pleading": CustomItem1,
|
||||
"purified_hand": CustomItem2,
|
||||
"start_wheel": StartWheel,
|
||||
"skill_randomizer": SkillRando,
|
||||
"enemy_randomizer": EnemyRando,
|
||||
"enemy_groups": EnemyGroups,
|
||||
"enemy_scaling": EnemyScaling,
|
||||
"death_link": BlasphemousDeathLink,
|
||||
"start_inventory": StartInventoryPool
|
||||
}
|
||||
@dataclass
|
||||
class BlasphemousOptions(PerGameCommonOptions):
|
||||
prie_dieu_warp: PrieDieuWarp
|
||||
skip_cutscenes: SkipCutscenes
|
||||
corpse_hints: CorpseHints
|
||||
difficulty: Difficulty
|
||||
penitence: Penitence
|
||||
starting_location: StartingLocation
|
||||
ending: Ending
|
||||
skip_long_quests: SkipLongQuests
|
||||
thorn_shuffle : ThornShuffle
|
||||
dash_shuffle: DashShuffle
|
||||
wall_climb_shuffle: WallClimbShuffle
|
||||
reliquary_shuffle: ReliquaryShuffle
|
||||
boots_of_pleading: CustomItem1
|
||||
purified_hand: CustomItem2
|
||||
start_wheel: StartWheel
|
||||
skill_randomizer: SkillRando
|
||||
enemy_randomizer: EnemyRando
|
||||
enemy_groups: EnemyGroups
|
||||
enemy_scaling: EnemyScaling
|
||||
death_link: BlasphemousDeathLink
|
||||
start_inventory: StartInventoryPool
|
||||
@@ -497,8 +497,9 @@ def chalice_rooms(state: CollectionState, player: int, number: int) -> bool:
|
||||
def rules(blasphemousworld):
|
||||
world = blasphemousworld.multiworld
|
||||
player = blasphemousworld.player
|
||||
logic = world.difficulty[player].value
|
||||
enemy = world.enemy_randomizer[player].value
|
||||
options = blasphemousworld.options
|
||||
logic = options.difficulty.value
|
||||
enemy = options.enemy_randomizer.value
|
||||
|
||||
|
||||
# D01Z01S01 (The Holy Line)
|
||||
@@ -2488,7 +2489,7 @@ def rules(blasphemousworld):
|
||||
|
||||
# D04Z02S01 (Mother of Mothers)
|
||||
# Items
|
||||
if world.purified_hand[player]:
|
||||
if options.purified_hand:
|
||||
set_rule(world.get_location("MoM: Western room ledge", player),
|
||||
lambda state: (
|
||||
state.has("D04Z02S01[N]", player)
|
||||
@@ -4093,7 +4094,7 @@ def rules(blasphemousworld):
|
||||
|
||||
# D17Z01S04 (Brotherhood of the Silent Sorrow)
|
||||
# Items
|
||||
if world.boots_of_pleading[player]:
|
||||
if options.boots_of_pleading:
|
||||
set_rule(world.get_location("BotSS: 2nd meeting with Redento", player),
|
||||
lambda state: redento(state, blasphemousworld, player, 2))
|
||||
# Doors
|
||||
|
||||
@@ -7,7 +7,7 @@ from .Locations import location_table
|
||||
from .Rooms import room_table, door_table
|
||||
from .Rules import rules
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Options import blasphemous_options
|
||||
from .Options import BlasphemousOptions
|
||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ class BlasphemousWorld(World):
|
||||
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
||||
|
||||
item_name_groups = group_table
|
||||
option_definitions = blasphemous_options
|
||||
options = BlasphemousOptions
|
||||
options_dataclass = BlasphemousOptions
|
||||
|
||||
required_client_version = (0, 4, 2)
|
||||
|
||||
@@ -73,60 +74,61 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
if not world.starting_location[player].randomized:
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
if not options.starting_location.randomized:
|
||||
if options.starting_location.value == 6 and options.difficulty.value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
|
||||
" cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
if (options.starting_location.value == 0 or options.starting_location.value == 6) \
|
||||
and options.dash_shuffle:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
|
||||
" cannot be chosen if Shuffle Dash is enabled.")
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
if options.starting_location.value == 3 and options.wall_climb_shuffle:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
|
||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
else:
|
||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||
invalid: bool = False
|
||||
|
||||
if world.difficulty[player].value < 2:
|
||||
if options.difficulty.value < 2:
|
||||
locations.remove(6)
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if options.dash_shuffle:
|
||||
locations.remove(0)
|
||||
if 6 in locations:
|
||||
locations.remove(6)
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if options.wall_climb_shuffle:
|
||||
locations.remove(3)
|
||||
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
if options.starting_location.value == 6 and options.difficulty.value < 2:
|
||||
invalid = True
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
if (options.starting_location.value == 0 or options.starting_location.value == 6) \
|
||||
and options.dash_shuffle:
|
||||
invalid = True
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
if options.starting_location.value == 3 and options.wall_climb_shuffle:
|
||||
invalid = True
|
||||
|
||||
if invalid:
|
||||
world.starting_location[player].value = world.random.choice(locations)
|
||||
options.starting_location.value = world.random.choice(locations)
|
||||
|
||||
|
||||
if not world.dash_shuffle[player]:
|
||||
if not options.dash_shuffle:
|
||||
world.push_precollected(self.create_item("Dash Ability"))
|
||||
|
||||
if not world.wall_climb_shuffle[player]:
|
||||
if not options.wall_climb_shuffle:
|
||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if world.skip_long_quests[player]:
|
||||
if options.skip_long_quests:
|
||||
for loc in junk_locations:
|
||||
world.exclude_locations[player].value.add(loc)
|
||||
options.exclude_locations.value.add(loc)
|
||||
|
||||
start_rooms: Dict[int, str] = {
|
||||
0: "D17Z01S01",
|
||||
@@ -138,12 +140,12 @@ class BlasphemousWorld(World):
|
||||
6: "D20Z02S09"
|
||||
}
|
||||
|
||||
self.start_room = start_rooms[world.starting_location[player].value]
|
||||
self.start_room = start_rooms[options.starting_location.value]
|
||||
|
||||
|
||||
def create_items(self):
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
removed: int = 0
|
||||
to_remove: List[str] = [
|
||||
@@ -157,46 +159,46 @@ class BlasphemousWorld(World):
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
for item, count in world.start_inventory[player].value.items():
|
||||
for item, count in options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
if options.thorn_shuffle == 2:
|
||||
for i in range(8):
|
||||
skipped_items.append("Thorn Upgrade")
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if options.dash_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.dash_shuffle[player]:
|
||||
elif not options.dash_shuffle:
|
||||
skipped_items.append("Dash Ability")
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if options.wall_climb_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.wall_climb_shuffle[player]:
|
||||
elif not options.wall_climb_shuffle:
|
||||
skipped_items.append("Wall Climb Ability")
|
||||
|
||||
if not world.reliquary_shuffle[player]:
|
||||
if not options.reliquary_shuffle:
|
||||
skipped_items.extend(reliquary_set)
|
||||
elif world.reliquary_shuffle[player]:
|
||||
elif options.reliquary_shuffle:
|
||||
for i in range(3):
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
|
||||
if not world.boots_of_pleading[player]:
|
||||
if not options.boots_of_pleading:
|
||||
skipped_items.append("Boots of Pleading")
|
||||
|
||||
if not world.purified_hand[player]:
|
||||
if not options.purified_hand:
|
||||
skipped_items.append("Purified Hand of the Nun")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
if options.start_wheel:
|
||||
skipped_items.append("The Young Mason's Wheel")
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not options.skill_randomizer:
|
||||
skipped_items.extend(skill_dict.values())
|
||||
|
||||
counter = Counter(skipped_items)
|
||||
@@ -219,23 +221,24 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
if options.thorn_shuffle == 2:
|
||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
if options.start_wheel:
|
||||
world.get_location("Beginning gift", player)\
|
||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 1:
|
||||
world.local_items[player].value.add("Thorn Upgrade")
|
||||
if options.thorn_shuffle == 1:
|
||||
options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
@@ -251,6 +254,7 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
options = self.options
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
|
||||
@@ -282,9 +286,9 @@ class BlasphemousWorld(World):
|
||||
})
|
||||
|
||||
for index, loc in enumerate(location_table):
|
||||
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
if not options.boots_of_pleading and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
continue
|
||||
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
|
||||
if not options.purified_hand and loc["name"] == "MoM: Western room ledge":
|
||||
continue
|
||||
|
||||
region: Region = world.get_region(loc["room"], player)
|
||||
@@ -310,9 +314,9 @@ class BlasphemousWorld(World):
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
world.get_region("D07Z01S03", player).locations.append(victory)
|
||||
|
||||
if world.ending[self.player].value == 1:
|
||||
if options.ending.value == 1:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||
elif world.ending[self.player].value == 2:
|
||||
elif options.ending.value == 2:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||
state.has("Holy Wound of Abnegation", player))
|
||||
|
||||
@@ -332,11 +336,12 @@ class BlasphemousWorld(World):
|
||||
locations = []
|
||||
doors: Dict[str, str] = {}
|
||||
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
thorns: bool = True
|
||||
|
||||
if world.thorn_shuffle[player].value == 2:
|
||||
if options.thorn_shuffle.value == 2:
|
||||
thorns = False
|
||||
|
||||
for loc in world.get_filled_locations(player):
|
||||
@@ -354,28 +359,28 @@ class BlasphemousWorld(World):
|
||||
locations.append(data)
|
||||
|
||||
config = {
|
||||
"LogicDifficulty": world.difficulty[player].value,
|
||||
"StartingLocation": world.starting_location[player].value,
|
||||
"LogicDifficulty": options.difficulty.value,
|
||||
"StartingLocation": options.starting_location.value,
|
||||
"VersionCreated": "AP",
|
||||
|
||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
||||
"AllowHints": bool(world.corpse_hints[player].value),
|
||||
"AllowPenitence": bool(world.penitence[player].value),
|
||||
"UnlockTeleportation": bool(options.prie_dieu_warp.value),
|
||||
"AllowHints": bool(options.corpse_hints.value),
|
||||
"AllowPenitence": bool(options.penitence.value),
|
||||
|
||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
||||
"ShuffleReliquaries": bool(options.reliquary_shuffle.value),
|
||||
"ShuffleBootsOfPleading": bool(options.boots_of_pleading.value),
|
||||
"ShufflePurifiedHand": bool(options.purified_hand.value),
|
||||
"ShuffleDash": bool(options.dash_shuffle.value),
|
||||
"ShuffleWallClimb": bool(options.wall_climb_shuffle.value),
|
||||
|
||||
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
||||
"ShuffleSwordSkills": bool(options.skill_randomizer.value),
|
||||
"ShuffleThorns": thorns,
|
||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
||||
"JunkLongQuests": bool(options.skip_long_quests.value),
|
||||
"StartWithWheel": bool(options.start_wheel.value),
|
||||
|
||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
||||
"EnemyShuffleType": options.enemy_randomizer.value,
|
||||
"MaintainClass": bool(options.enemy_groups.value),
|
||||
"AreaScaling": bool(options.enemy_scaling.value),
|
||||
|
||||
"BossShuffleType": 0,
|
||||
"DoorShuffleType": 0
|
||||
@@ -385,8 +390,8 @@ class BlasphemousWorld(World):
|
||||
"locations": locations,
|
||||
"doors": doors,
|
||||
"cfg": config,
|
||||
"ending": world.ending[self.player].value,
|
||||
"death_link": bool(world.death_link[self.player].value)
|
||||
"ending": options.ending.value,
|
||||
"death_link": bool(options.death_link.value)
|
||||
}
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
import typing
|
||||
from Options import Option, Range
|
||||
from Options import Option, Range, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class TaskAdvances(Range):
|
||||
@@ -69,12 +70,12 @@ class KillerTrapWeight(Range):
|
||||
default = 0
|
||||
|
||||
|
||||
bumpstik_options: typing.Dict[str, type(Option)] = {
|
||||
"task_advances": TaskAdvances,
|
||||
"turners": Turners,
|
||||
"paint_cans": PaintCans,
|
||||
"trap_count": Traps,
|
||||
"rainbow_trap_weight": RainbowTrapWeight,
|
||||
"spinner_trap_weight": SpinnerTrapWeight,
|
||||
"killer_trap_weight": KillerTrapWeight
|
||||
}
|
||||
@dataclass
|
||||
class BumpStikOptions(PerGameCommonOptions):
|
||||
task_advances: TaskAdvances
|
||||
turners: Turners
|
||||
paint_cans: PaintCans
|
||||
trap_count: Traps
|
||||
rainbow_trap_weight: RainbowTrapWeight
|
||||
spinner_trap_weight: SpinnerTrapWeight
|
||||
killer_trap_weight: KillerTrapWeight
|
||||
|
||||
@@ -43,7 +43,8 @@ class BumpStikWorld(World):
|
||||
|
||||
required_client_version = (0, 3, 8)
|
||||
|
||||
option_definitions = bumpstik_options
|
||||
options = BumpStikOptions
|
||||
options_dataclass = BumpStikOptions
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super(BumpStikWorld, self).__init__(world, player)
|
||||
@@ -86,13 +87,13 @@ class BumpStikWorld(World):
|
||||
return "Nothing"
|
||||
|
||||
def generate_early(self):
|
||||
self.task_advances = self.multiworld.task_advances[self.player].value
|
||||
self.turners = self.multiworld.turners[self.player].value
|
||||
self.paint_cans = self.multiworld.paint_cans[self.player].value
|
||||
self.traps = self.multiworld.trap_count[self.player].value
|
||||
self.rainbow_trap_weight = self.multiworld.rainbow_trap_weight[self.player].value
|
||||
self.spinner_trap_weight = self.multiworld.spinner_trap_weight[self.player].value
|
||||
self.killer_trap_weight = self.multiworld.killer_trap_weight[self.player].value
|
||||
self.task_advances = self.options.task_advances.value
|
||||
self.turners = self.options.turners.value
|
||||
self.paint_cans = self.options.paint_cans.value
|
||||
self.traps = self.options.trap_count.value
|
||||
self.rainbow_trap_weight = self.options.rainbow_trap_weight.value
|
||||
self.spinner_trap_weight = self.options.spinner_trap_weight.value
|
||||
self.killer_trap_weight = self.options.killer_trap_weight.value
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.multiworld, self.player)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
from Options import Option
|
||||
from Options import Option, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
checksfinder_options: typing.Dict[str, type(Option)] = {
|
||||
}
|
||||
@dataclass
|
||||
class ChecksFinderOptions(PerGameCommonOptions):
|
||||
pass
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification
|
||||
from .Items import ChecksFinderItem, item_table, required_items
|
||||
from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table
|
||||
from .Options import checksfinder_options
|
||||
from .Options import ChecksFinderOptions
|
||||
from .Rules import set_rules, set_completion_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from dataclasses import fields
|
||||
|
||||
client_version = 7
|
||||
|
||||
@@ -26,7 +27,8 @@ class ChecksFinderWorld(World):
|
||||
with the mines! You win when you get all your items and beat the board!
|
||||
"""
|
||||
game: str = "ChecksFinder"
|
||||
option_definitions = checksfinder_options
|
||||
options = ChecksFinderOptions
|
||||
options_dataclass = ChecksFinderOptions
|
||||
topology_present = True
|
||||
web = ChecksFinderWeb()
|
||||
|
||||
@@ -79,8 +81,8 @@ class ChecksFinderWorld(World):
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = self._get_checksfinder_data()
|
||||
for option_name in checksfinder_options:
|
||||
option = getattr(self.multiworld, option_name)[self.player]
|
||||
for option_name in [field.name for field in fields(ChecksFinderOptions)]:
|
||||
option = getattr(self.options, option_name)
|
||||
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
|
||||
slot_data[option_name] = int(option.value)
|
||||
return slot_data
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Options import CliqueOptions
|
||||
|
||||
|
||||
class CliqueItem(Item):
|
||||
@@ -10,7 +11,7 @@ class CliqueItem(Item):
|
||||
class CliqueItemData(NamedTuple):
|
||||
code: Optional[int] = None
|
||||
type: ItemClassification = ItemClassification.filler
|
||||
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||
can_create: Callable[[CliqueOptions], bool] = lambda options: True
|
||||
|
||||
|
||||
item_data_table: Dict[str, CliqueItemData] = {
|
||||
@@ -21,11 +22,11 @@ item_data_table: Dict[str, CliqueItemData] = {
|
||||
"Button Activation": CliqueItemData(
|
||||
code=69696968,
|
||||
type=ItemClassification.progression,
|
||||
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||
can_create=lambda options: bool(getattr(options, "hard_mode")),
|
||||
),
|
||||
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
|
||||
code=69696967,
|
||||
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
|
||||
can_create=lambda options: False # Only created from `get_filler_item_name`.
|
||||
),
|
||||
"The Urge to Push": CliqueItemData(
|
||||
type=ItemClassification.progression,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Location, MultiWorld
|
||||
from BaseClasses import Location
|
||||
from .Options import CliqueOptions
|
||||
|
||||
|
||||
|
||||
class CliqueLocation(Location):
|
||||
@@ -10,7 +12,7 @@ class CliqueLocation(Location):
|
||||
class CliqueLocationData(NamedTuple):
|
||||
region: str
|
||||
address: Optional[int] = None
|
||||
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||
can_create: Callable[[CliqueOptions], bool] = lambda options: True
|
||||
locked_item: Optional[str] = None
|
||||
|
||||
|
||||
@@ -22,7 +24,7 @@ location_data_table: Dict[str, CliqueLocationData] = {
|
||||
"The Item on the Desk": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
address=69696968,
|
||||
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||
can_create=lambda options: bool(getattr(options, "hard_mode")),
|
||||
),
|
||||
"In the Player's Mind": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, Toggle
|
||||
from Options import Choice, Option, Toggle, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class HardMode(Toggle):
|
||||
@@ -25,10 +26,12 @@ class ButtonColor(Choice):
|
||||
option_black = 11
|
||||
|
||||
|
||||
clique_options: Dict[str, type(Option)] = {
|
||||
"color": ButtonColor,
|
||||
"hard_mode": HardMode,
|
||||
|
||||
@dataclass
|
||||
class CliqueOptions(PerGameCommonOptions):
|
||||
color: ButtonColor
|
||||
hard_mode: HardMode
|
||||
|
||||
# DeathLink is always on. Always.
|
||||
# "death_link": DeathLink,
|
||||
}
|
||||
# death_link: DeathLink
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from typing import Callable
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from BaseClasses import CollectionState
|
||||
from .Options import CliqueOptions
|
||||
|
||||
|
||||
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]:
|
||||
if getattr(multiworld, "hard_mode")[player]:
|
||||
def get_button_rule(options: CliqueOptions, player: int) -> Callable[[CollectionState], bool]:
|
||||
if getattr(options, "hard_mode"):
|
||||
return lambda state: state.has("Button Activation", player)
|
||||
|
||||
return lambda state: True
|
||||
|
||||
@@ -4,7 +4,7 @@ from BaseClasses import Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import CliqueItem, item_data_table, item_table
|
||||
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
|
||||
from .Options import clique_options
|
||||
from .Options import CliqueOptions
|
||||
from .Regions import region_data_table
|
||||
from .Rules import get_button_rule
|
||||
|
||||
@@ -29,7 +29,8 @@ class CliqueWorld(World):
|
||||
game = "Clique"
|
||||
data_version = 3
|
||||
web = CliqueWebWorld()
|
||||
option_definitions = clique_options
|
||||
options = CliqueOptions
|
||||
options_dataclass = CliqueOptions
|
||||
location_name_to_id = location_table
|
||||
item_name_to_id = item_table
|
||||
|
||||
@@ -39,7 +40,7 @@ class CliqueWorld(World):
|
||||
def create_items(self) -> None:
|
||||
item_pool: List[CliqueItem] = []
|
||||
for name, item in item_data_table.items():
|
||||
if item.code and item.can_create(self.multiworld, self.player):
|
||||
if item.code and item.can_create(self.options):
|
||||
item_pool.append(self.create_item(name))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
@@ -55,27 +56,27 @@ class CliqueWorld(World):
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
region.add_locations({
|
||||
location_name: location_data.address for location_name, location_data in location_data_table.items()
|
||||
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player)
|
||||
if location_data.region == region_name and location_data.can_create(self.options)
|
||||
}, CliqueLocation)
|
||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||
|
||||
# Place locked locations.
|
||||
for location_name, location_data in locked_locations.items():
|
||||
# Ignore locations we never created.
|
||||
if not location_data.can_create(self.multiworld, self.player):
|
||||
if not location_data.can_create(self.options):
|
||||
continue
|
||||
|
||||
locked_item = self.create_item(location_data_table[location_name].locked_item)
|
||||
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
|
||||
|
||||
# Set priority location for the Big Red Button!
|
||||
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
|
||||
self.options.priority_locations.value.add("The Big Red Button")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "A Cool Filler Item (No Satisfaction Guaranteed)"
|
||||
|
||||
def set_rules(self) -> None:
|
||||
button_rule = get_button_rule(self.multiworld, self.player)
|
||||
button_rule = get_button_rule(self.options, self.player)
|
||||
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
|
||||
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
|
||||
|
||||
@@ -88,5 +89,5 @@ class CliqueWorld(World):
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"color": getattr(self.multiworld, "color")[self.player].current_key
|
||||
"color": getattr(self.options, "color").current_key
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
|
||||
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
|
||||
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class RandomizeWeaponLocations(DefaultOnToggle):
|
||||
@@ -200,36 +201,36 @@ class EnableDLCOption(Toggle):
|
||||
display_name = "Enable DLC"
|
||||
|
||||
|
||||
dark_souls_options: typing.Dict[str, Option] = {
|
||||
"enable_weapon_locations": RandomizeWeaponLocations,
|
||||
"enable_shield_locations": RandomizeShieldLocations,
|
||||
"enable_armor_locations": RandomizeArmorLocations,
|
||||
"enable_ring_locations": RandomizeRingLocations,
|
||||
"enable_spell_locations": RandomizeSpellLocations,
|
||||
"enable_key_locations": RandomizeKeyLocations,
|
||||
"enable_boss_locations": RandomizeBossSoulLocations,
|
||||
"enable_npc_locations": RandomizeNPCLocations,
|
||||
"enable_misc_locations": RandomizeMiscLocations,
|
||||
"enable_health_upgrade_locations": RandomizeHealthLocations,
|
||||
"enable_progressive_locations": RandomizeProgressiveLocationsOption,
|
||||
"pool_type": PoolTypeOption,
|
||||
"guaranteed_items": GuaranteedItemsOption,
|
||||
"auto_equip": AutoEquipOption,
|
||||
"lock_equip": LockEquipOption,
|
||||
"no_weapon_requirements": NoWeaponRequirementsOption,
|
||||
"randomize_infusion": RandomizeInfusionOption,
|
||||
"randomize_infusion_percentage": RandomizeInfusionPercentageOption,
|
||||
"randomize_weapon_level": RandomizeWeaponLevelOption,
|
||||
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption,
|
||||
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
|
||||
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
|
||||
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
|
||||
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
|
||||
"early_banner": EarlySmallLothricBanner,
|
||||
"late_basin_of_vows": LateBasinOfVowsOption,
|
||||
"late_dlc": LateDLCOption,
|
||||
"no_spell_requirements": NoSpellRequirementsOption,
|
||||
"no_equip_load": NoEquipLoadOption,
|
||||
"death_link": DeathLink,
|
||||
"enable_dlc": EnableDLCOption,
|
||||
}
|
||||
@dataclass
|
||||
class DarkSouls3Options(PerGameCommonOptions):
|
||||
enable_weapon_locations: RandomizeWeaponLocations
|
||||
enable_shield_locations: RandomizeShieldLocations
|
||||
enable_armor_locations: RandomizeArmorLocations
|
||||
enable_ring_locations: RandomizeRingLocations
|
||||
enable_spell_locations: RandomizeSpellLocations
|
||||
enable_key_locations: RandomizeKeyLocations
|
||||
enable_boss_locations: RandomizeBossSoulLocations
|
||||
enable_npc_locations: RandomizeNPCLocations
|
||||
enable_misc_locations: RandomizeMiscLocations
|
||||
enable_health_upgrade_locations: RandomizeHealthLocations
|
||||
enable_progressive_locations: RandomizeProgressiveLocationsOption
|
||||
pool_type: PoolTypeOption
|
||||
guaranteed_items: GuaranteedItemsOption
|
||||
auto_equip: AutoEquipOption
|
||||
lock_equip: LockEquipOption
|
||||
no_weapon_requirements: NoWeaponRequirementsOption
|
||||
randomize_infusion: RandomizeInfusionOption
|
||||
randomize_infusion_percentage: RandomizeInfusionPercentageOption
|
||||
randomize_weapon_level: RandomizeWeaponLevelOption
|
||||
randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption
|
||||
min_levels_in_5: MinLevelsIn5WeaponPoolOption
|
||||
max_levels_in_5: MaxLevelsIn5WeaponPoolOption
|
||||
min_levels_in_10: MinLevelsIn10WeaponPoolOption
|
||||
max_levels_in_10: MaxLevelsIn10WeaponPoolOption
|
||||
early_banner: EarlySmallLothricBanner
|
||||
late_basin_of_vows: LateBasinOfVowsOption
|
||||
late_dlc: LateDLCOption
|
||||
no_spell_requirements: NoSpellRequirementsOption
|
||||
no_equip_load: NoEquipLoadOption
|
||||
death_link: DeathLink
|
||||
enable_dlc: EnableDLCOption
|
||||
|
||||
@@ -9,7 +9,7 @@ from worlds.generic.Rules import set_rule, add_rule, add_item_rule
|
||||
|
||||
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions
|
||||
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, DarkSouls3Options
|
||||
|
||||
|
||||
class DarkSouls3Web(WebWorld):
|
||||
@@ -43,7 +43,8 @@ class DarkSouls3World(World):
|
||||
"""
|
||||
|
||||
game: str = "Dark Souls III"
|
||||
option_definitions = dark_souls_options
|
||||
options = DarkSouls3Options
|
||||
options_dataclass = DarkSouls3Options
|
||||
topology_present: bool = True
|
||||
web = DarkSouls3Web()
|
||||
data_version = 8
|
||||
@@ -72,47 +73,47 @@ class DarkSouls3World(World):
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_weapon_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.WEAPON)
|
||||
if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_shield_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.SHIELD)
|
||||
if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_armor_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.ARMOR)
|
||||
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_ring_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.RING)
|
||||
if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_spell_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.SPELL)
|
||||
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_npc_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.NPC)
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_key_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.KEY)
|
||||
if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global:
|
||||
self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1
|
||||
elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local:
|
||||
self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
if self.options.early_banner == EarlySmallLothricBanner.option_early_global:
|
||||
self.options.early_items['Small Lothric Banner'] = 1
|
||||
elif self.options.early_banner == EarlySmallLothricBanner.option_early_local:
|
||||
self.options.local_early_items['Small Lothric Banner'] = 1
|
||||
if self.options.enable_boss_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_misc_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.MISC)
|
||||
if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_health_upgrade_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.HEALTH)
|
||||
if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_progressive_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM)
|
||||
|
||||
|
||||
def create_regions(self):
|
||||
progressive_location_table = []
|
||||
if self.multiworld.enable_progressive_locations[self.player]:
|
||||
if self.options.enable_progressive_locations:
|
||||
progressive_location_table = [] + \
|
||||
location_tables["Progressive Items 1"] + \
|
||||
location_tables["Progressive Items 2"] + \
|
||||
location_tables["Progressive Items 3"] + \
|
||||
location_tables["Progressive Items 4"]
|
||||
|
||||
if self.multiworld.enable_dlc[self.player].value:
|
||||
if self.options.enable_dlc.value:
|
||||
progressive_location_table += location_tables["Progressive Items DLC"]
|
||||
|
||||
if self.multiworld.enable_health_upgrade_locations[self.player]:
|
||||
if self.options.enable_health_upgrade_locations:
|
||||
progressive_location_table += location_tables["Progressive Items Health"]
|
||||
|
||||
# Create Vanilla Regions
|
||||
@@ -146,7 +147,7 @@ class DarkSouls3World(World):
|
||||
regions["Consumed King's Garden"].locations.append(potd_location)
|
||||
|
||||
# Create DLC Regions
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
if self.options.enable_dlc:
|
||||
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
|
||||
"Painted World of Ariandel 1",
|
||||
"Painted World of Ariandel 2",
|
||||
@@ -192,7 +193,7 @@ class DarkSouls3World(World):
|
||||
create_connection("Consumed King's Garden", "Untended Graves")
|
||||
|
||||
# Connect DLC Regions
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
if self.options.enable_dlc:
|
||||
create_connection("Cathedral of the Deep", "Painted World of Ariandel 1")
|
||||
create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2")
|
||||
create_connection("Painted World of Ariandel 2", "Dreg Heap")
|
||||
@@ -240,7 +241,7 @@ class DarkSouls3World(World):
|
||||
|
||||
|
||||
def create_items(self):
|
||||
dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true
|
||||
dlc_enabled = self.options.enable_dlc == Toggle.option_true
|
||||
|
||||
itempool_by_category = {category: [] for category in self.enabled_location_categories}
|
||||
|
||||
@@ -254,7 +255,7 @@ class DarkSouls3World(World):
|
||||
itempool_by_category[location.category].append(location.default_item_name)
|
||||
|
||||
# Replace each item category with a random sample of items of those types
|
||||
if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various:
|
||||
if self.options.pool_type == PoolTypeOption.option_various:
|
||||
def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int):
|
||||
candidates = [
|
||||
item.name for item
|
||||
@@ -300,7 +301,7 @@ class DarkSouls3World(World):
|
||||
# A list of items we can replace
|
||||
removable_items = [item for item in itempool if item.classification != ItemClassification.progression]
|
||||
|
||||
guaranteed_items = self.multiworld.guaranteed_items[self.player].value
|
||||
guaranteed_items = self.options.guaranteed_items.value
|
||||
for item_name in guaranteed_items:
|
||||
# Break early just in case nothing is removable (if user is trying to guarantee more
|
||||
# items than the pool can hold, for example)
|
||||
@@ -384,22 +385,22 @@ class DarkSouls3World(World):
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and
|
||||
state.has("Cinders of a Lord - Lothric Prince", self.player))
|
||||
|
||||
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
|
||||
if self.options.late_basin_of_vows == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
# DLC Access Rules Below
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
if self.options.enable_dlc:
|
||||
set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player),
|
||||
lambda state: state.has("Small Envoy Banner", self.player))
|
||||
|
||||
# If key items are randomized, must have contraption key to enter second half of Ashes DLC
|
||||
# If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_key_locations == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player),
|
||||
lambda state: state.has("Contraption Key", self.player))
|
||||
|
||||
if self.multiworld.late_dlc[self.player] == Toggle.option_true:
|
||||
if self.options.late_dlc == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player),
|
||||
lambda state: state.has("Small Doll", self.player))
|
||||
|
||||
@@ -407,7 +408,7 @@ class DarkSouls3World(World):
|
||||
set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
|
||||
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_ring_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
|
||||
lambda state: state.has("Jailbreaker's Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
|
||||
@@ -415,7 +416,7 @@ class DarkSouls3World(World):
|
||||
set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_npc_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
|
||||
@@ -431,11 +432,11 @@ class DarkSouls3World(World):
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_misc_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_boss_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
@@ -443,7 +444,7 @@ class DarkSouls3World(World):
|
||||
|
||||
# Lump Soul of the Dancer in with LC for locations that should not be reachable
|
||||
# before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US)
|
||||
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
|
||||
if self.options.late_basin_of_vows == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
@@ -453,10 +454,10 @@ class DarkSouls3World(World):
|
||||
|
||||
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule)
|
||||
|
||||
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_weapon_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
self.options.completion_condition = lambda state: \
|
||||
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
|
||||
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and \
|
||||
@@ -470,13 +471,13 @@ class DarkSouls3World(World):
|
||||
name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()}
|
||||
|
||||
# Randomize some weapon upgrades
|
||||
if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none:
|
||||
if self.options.randomize_weapon_level != RandomizeWeaponLevelOption.option_none:
|
||||
# if the user made an error and set a min higher than the max we default to the max
|
||||
max_5 = self.multiworld.max_levels_in_5[self.player]
|
||||
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5)
|
||||
max_10 = self.multiworld.max_levels_in_10[self.player]
|
||||
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10)
|
||||
weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player]
|
||||
max_5 = self.options.max_levels_in_5
|
||||
min_5 = min(self.options.min_levels_in_5, max_5)
|
||||
max_10 = self.options.max_levels_in_10
|
||||
min_10 = min(self.options.min_levels_in_10, max_10)
|
||||
weapon_level_percentage = self.options.randomize_weapon_level_percentage
|
||||
|
||||
for item in item_dictionary.values():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage:
|
||||
@@ -486,8 +487,8 @@ class DarkSouls3World(World):
|
||||
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
|
||||
# Randomize some weapon infusions
|
||||
if self.multiworld.randomize_infusion[self.player] == Toggle.option_true:
|
||||
infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player]
|
||||
if self.options.randomize_infusion == Toggle.option_true:
|
||||
infusion_percentage = self.options.randomize_infusion_percentage
|
||||
for item in item_dictionary.values():
|
||||
if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}:
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage:
|
||||
@@ -518,22 +519,22 @@ class DarkSouls3World(World):
|
||||
|
||||
slot_data = {
|
||||
"options": {
|
||||
"enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value,
|
||||
"enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value,
|
||||
"enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value,
|
||||
"enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value,
|
||||
"enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value,
|
||||
"enable_key_locations": self.multiworld.enable_key_locations[self.player].value,
|
||||
"enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value,
|
||||
"enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value,
|
||||
"enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value,
|
||||
"auto_equip": self.multiworld.auto_equip[self.player].value,
|
||||
"lock_equip": self.multiworld.lock_equip[self.player].value,
|
||||
"no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value,
|
||||
"death_link": self.multiworld.death_link[self.player].value,
|
||||
"no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value,
|
||||
"no_equip_load": self.multiworld.no_equip_load[self.player].value,
|
||||
"enable_dlc": self.multiworld.enable_dlc[self.player].value
|
||||
"enable_weapon_locations": self.options.enable_weapon_locations.value,
|
||||
"enable_shield_locations": self.options.enable_shield_locations.value,
|
||||
"enable_armor_locations": self.options.enable_armor_locations.value,
|
||||
"enable_ring_locations": self.options.enable_ring_locations.value,
|
||||
"enable_spell_locations": self.options.enable_spell_locations.value,
|
||||
"enable_key_locations": self.options.enable_key_locations.value,
|
||||
"enable_boss_locations": self.options.enable_boss_locations.value,
|
||||
"enable_npc_locations": self.options.enable_npc_locations.value,
|
||||
"enable_misc_locations": self.options.enable_misc_locations.value,
|
||||
"auto_equip": self.options.auto_equip.value,
|
||||
"lock_equip": self.options.lock_equip.value,
|
||||
"no_weapon_requirements": self.options.no_weapon_requirements.value,
|
||||
"death_link": self.options.death_link.value,
|
||||
"no_spell_requirements": self.options.no_spell_requirements.value,
|
||||
"no_equip_load": self.options.no_equip_load.value,
|
||||
"enable_dlc": self.options.enable_dlc.value
|
||||
},
|
||||
"seed": self.multiworld.seed_name, # to verify the server's multiworld
|
||||
"slot": self.multiworld.player_name[self.player], # to connect to server
|
||||
|
||||
@@ -21,7 +21,20 @@ This client has only been tested with the Official Steam version of the game at
|
||||
|
||||
## Downpatching Dark Souls III
|
||||
|
||||
Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333"
|
||||
To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database.
|
||||
|
||||
1. Launch Steam (in online mode).
|
||||
2. Press the Windows Key + R. This will open the Run window.
|
||||
3. Open the Steam console by typing the following string: steam://open/console , Steam should now open in Console Mode.
|
||||
4. Insert the string of the depot you wish to download. For the AP supported v1.15, you will want to use: download_depot 374320 374321 4471176929659548333.
|
||||
5. Steam will now download the depot. Note: There is no progress bar of the download in Steam, but it is still downloading in the background.
|
||||
6. Turn off auto-updates in Steam by right-clicking Dark Souls III in your library > Properties > Updates > set "Automatic Updates" to "Only update this game when I launch it" (or change the value for AutoUpdateBehavior to 1 in "\Steam\steamapps\appmanifest_374320.acf").
|
||||
7. Back up your existing game folder in "\Steam\steamapps\common\DARK SOULS III".
|
||||
8. Return back to Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX". Back up this game folder as well.
|
||||
9. Delete your existing game folder in "\Steam\steamapps\common\DARK SOULS III", then replace it with your game folder in "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX".
|
||||
10. Back up and delete your save file "DS30000.sl2" in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type %appdata% and hit enter or: open File Explorer > View > Hidden Items and follow "C:\Users\your username\AppData\Roaming\DarkSoulsIII\numbers".
|
||||
11. If you did all these steps correctly, you should be able to confirm your game version in the upper left corner after launching Dark Souls III.
|
||||
|
||||
|
||||
## Installing the Archipelago mod
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import os
|
||||
import shutil
|
||||
import threading
|
||||
import zipfile
|
||||
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple
|
||||
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
|
||||
from dataclasses import fields
|
||||
import datetime
|
||||
|
||||
import jinja2
|
||||
|
||||
@@ -63,7 +65,7 @@ recipe_time_ranges = {
|
||||
class FactorioModFile(worlds.Files.APContainer):
|
||||
game = "Factorio"
|
||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||
writing_tasks: List[Callable[[], Tuple[str, str]]]
|
||||
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -88,6 +90,7 @@ class FactorioModFile(worlds.Files.APContainer):
|
||||
def generate_mod(world: "Factorio", output_directory: str):
|
||||
player = world.player
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
global data_final_template, locale_template, control_template, data_template, settings_template
|
||||
with template_load_lock:
|
||||
if not data_final_template:
|
||||
@@ -129,44 +132,42 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
"base_tech_table": base_tech_table,
|
||||
"tech_to_progressive_lookup": tech_to_progressive_lookup,
|
||||
"mod_name": mod_name,
|
||||
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
||||
"allowed_science_packs": options.max_science_pack.get_allowed_packs(),
|
||||
"custom_technologies": multiworld.worlds[player].custom_technologies,
|
||||
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
|
||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
|
||||
"slot_player": player,
|
||||
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
||||
"starting_items": options.starting_items, "recipes": recipes,
|
||||
"random": random, "flop_random": flop_random,
|
||||
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
|
||||
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
|
||||
"recipe_time_scale": recipe_time_scales.get(options.recipe_time.value, None),
|
||||
"recipe_time_range": recipe_time_ranges.get(options.recipe_time.value, None),
|
||||
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
|
||||
"progressive_technology_table": {tech.name: tech.progressive for tech in
|
||||
progressive_technology_table.values()},
|
||||
"custom_recipes": world.custom_recipes,
|
||||
"max_science_pack": multiworld.max_science_pack[player].value,
|
||||
"max_science_pack": options.max_science_pack.value,
|
||||
"liquids": fluids,
|
||||
"goal": multiworld.goal[player].value,
|
||||
"energy_link": multiworld.energy_link[player].value,
|
||||
"goal": options.goal.value,
|
||||
"energy_link": options.energy_link.value,
|
||||
"useless_technologies": useless_technologies,
|
||||
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
|
||||
"chunk_shuffle": options.chunk_shuffle.value if datetime.datetime.today().month == 4 else 0,
|
||||
}
|
||||
|
||||
for factorio_option in Options.factorio_options:
|
||||
for factorio_option in [field.name for field in fields(Options.FactorioOptions)]:
|
||||
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
|
||||
continue
|
||||
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
||||
template_data[factorio_option] = getattr(options, factorio_option).value
|
||||
|
||||
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
||||
if getattr(options, "silo").value == Options.Silo.option_randomize_recipe:
|
||||
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
||||
|
||||
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
|
||||
if getattr(options, "satellite").value == Options.Satellite.option_randomize_recipe:
|
||||
template_data["free_sample_blacklist"]["satellite"] = 1
|
||||
|
||||
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
|
||||
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
|
||||
template_data["free_sample_blacklist"].update({item: 1 for item in options.free_sample_blacklist.value})
|
||||
template_data["free_sample_blacklist"].update({item: 0 for item in options.free_sample_whitelist.value})
|
||||
|
||||
mod_dir = os.path.join(output_directory, versioned_mod_name)
|
||||
|
||||
zf_path = os.path.join(mod_dir + ".zip")
|
||||
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
|
||||
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
|
||||
|
||||
if world.zip_path:
|
||||
@@ -177,7 +178,13 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file):
|
||||
(arcpath, content))
|
||||
else:
|
||||
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
|
||||
basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
|
||||
for dirpath, dirnames, filenames in os.walk(basepath):
|
||||
base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\")
|
||||
for filename in filenames:
|
||||
mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
|
||||
file_path=os.path.join(dirpath, filename):
|
||||
(arcpath, open(file_path, "rb").read()))
|
||||
|
||||
mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua",
|
||||
data_template.render(**template_data)))
|
||||
@@ -197,5 +204,3 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
|
||||
# write the mod file
|
||||
mod.write()
|
||||
# clean up
|
||||
shutil.rmtree(mod_dir)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool
|
||||
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool, PerGameCommonOptions
|
||||
from schema import Schema, Optional, And, Or
|
||||
from dataclasses import dataclass
|
||||
|
||||
# schema helpers
|
||||
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
|
||||
@@ -207,10 +207,9 @@ class RecipeIngredientsOffset(Range):
|
||||
range_end = 5
|
||||
|
||||
|
||||
class FactorioStartItems(ItemDict):
|
||||
class FactorioStartItems(OptionDict):
|
||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||
display_name = "Starting Items"
|
||||
verify_item_name = False
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
@@ -423,50 +422,44 @@ class EnergyLink(Toggle):
|
||||
display_name = "EnergyLink"
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {
|
||||
"max_science_pack": MaxSciencePack,
|
||||
"goal": Goal,
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"min_tech_cost": MinTechCost,
|
||||
"max_tech_cost": MaxTechCost,
|
||||
"tech_cost_distribution": TechCostDistribution,
|
||||
"tech_cost_mix": TechCostMix,
|
||||
"ramping_tech_costs": RampingTechCosts,
|
||||
"silo": Silo,
|
||||
"satellite": Satellite,
|
||||
"free_samples": FreeSamples,
|
||||
"tech_tree_information": TechTreeInformation,
|
||||
"starting_items": FactorioStartItems,
|
||||
"free_sample_blacklist": FactorioFreeSampleBlacklist,
|
||||
"free_sample_whitelist": FactorioFreeSampleWhitelist,
|
||||
"recipe_time": RecipeTime,
|
||||
"recipe_ingredients": RecipeIngredients,
|
||||
"recipe_ingredients_offset": RecipeIngredientsOffset,
|
||||
"imported_blueprints": ImportedBlueprint,
|
||||
"world_gen": FactorioWorldGen,
|
||||
"progressive": Progressive,
|
||||
"teleport_traps": TeleportTrapCount,
|
||||
"grenade_traps": GrenadeTrapCount,
|
||||
"cluster_grenade_traps": ClusterGrenadeTrapCount,
|
||||
"artillery_traps": ArtilleryTrapCount,
|
||||
"atomic_rocket_traps": AtomicRocketTrapCount,
|
||||
"attack_traps": AttackTrapCount,
|
||||
"evolution_traps": EvolutionTrapCount,
|
||||
"evolution_trap_increase": EvolutionTrapIncrease,
|
||||
"death_link": DeathLink,
|
||||
"energy_link": EnergyLink,
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
}
|
||||
|
||||
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
|
||||
if datetime.datetime.today().month == 4:
|
||||
|
||||
class ChunkShuffle(Toggle):
|
||||
"""Entrance Randomizer."""
|
||||
display_name = "Chunk Shuffle"
|
||||
class ChunkShuffle(Toggle):
|
||||
"""Entrance Randomizer.
|
||||
2023 April Fool's option. Shuffles chunk border transitions.
|
||||
Only valid during the Month of April. Forced off otherwise."""
|
||||
|
||||
|
||||
if datetime.datetime.today().day > 1:
|
||||
ChunkShuffle.__doc__ += """
|
||||
2023 April Fool's option. Shuffles chunk border transitions."""
|
||||
factorio_options["chunk_shuffle"] = ChunkShuffle
|
||||
@dataclass
|
||||
class FactorioOptions(PerGameCommonOptions):
|
||||
max_science_pack: MaxSciencePack
|
||||
goal: Goal
|
||||
tech_tree_layout: TechTreeLayout
|
||||
min_tech_cost: MinTechCost
|
||||
max_tech_cost: MaxTechCost
|
||||
tech_cost_distribution: TechCostDistribution
|
||||
tech_cost_mix: TechCostMix
|
||||
ramping_tech_costs: RampingTechCosts
|
||||
silo: Silo
|
||||
satellite: Satellite
|
||||
free_samples: FreeSamples
|
||||
tech_tree_information: TechTreeInformation
|
||||
starting_items: FactorioStartItems
|
||||
free_sample_blacklist: FactorioFreeSampleBlacklist
|
||||
free_sample_whitelist: FactorioFreeSampleWhitelist
|
||||
recipe_time: RecipeTime
|
||||
recipe_ingredients: RecipeIngredients
|
||||
recipe_ingredients_offset: RecipeIngredientsOffset
|
||||
imported_blueprints: ImportedBlueprint
|
||||
world_gen: FactorioWorldGen
|
||||
progressive: Progressive
|
||||
teleport_traps: TeleportTrapCount
|
||||
grenade_traps: GrenadeTrapCount
|
||||
cluster_grenade_traps: ClusterGrenadeTrapCount
|
||||
artillery_traps: ArtilleryTrapCount
|
||||
atomic_rocket_traps: AtomicRocketTrapCount
|
||||
attack_traps: AttackTrapCount
|
||||
evolution_traps: EvolutionTrapCount
|
||||
evolution_trap_increase: EvolutionTrapIncrease
|
||||
death_link: DeathLink
|
||||
energy_link: EnergyLink
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
chunk_shuffle: ChunkShuffle
|
||||
|
||||
@@ -20,10 +20,10 @@ def _sorter(location: "FactorioScienceLocation"):
|
||||
|
||||
|
||||
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
|
||||
options = factorio_world.options
|
||||
world = factorio_world.multiworld
|
||||
player = factorio_world.player
|
||||
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
|
||||
layout = world.tech_tree_layout[player].value
|
||||
layout = options.tech_tree_layout.value
|
||||
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
|
||||
world.random.shuffle(locations)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from worlds.LauncherComponents import Component, components, Type, launch_subpro
|
||||
from worlds.generic import Rules
|
||||
from .Locations import location_pools, location_table
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
|
||||
from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution, TechCostMix
|
||||
from .Shapes import get_shapes
|
||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||
@@ -88,6 +88,9 @@ class Factorio(World):
|
||||
location_pool: typing.List[FactorioScienceLocation]
|
||||
advancement_technologies: typing.Set[str]
|
||||
|
||||
options = FactorioOptions
|
||||
options_dataclass = FactorioOptions
|
||||
|
||||
web = FactorioWeb()
|
||||
|
||||
item_name_to_id = all_items
|
||||
@@ -117,11 +120,11 @@ class Factorio(World):
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# if max < min, then swap max and min
|
||||
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
|
||||
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
|
||||
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
|
||||
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
|
||||
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
|
||||
if self.options.max_tech_cost < self.options.min_tech_cost:
|
||||
self.options.min_tech_cost.value, self.options.max_tech_cost.value = \
|
||||
self.options.max_tech_cost.value, self.options.min_tech_cost.value
|
||||
self.tech_mix = self.options.tech_cost_mix
|
||||
self.skip_silo = self.options.silo.value == Silo.option_spawn
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
@@ -132,17 +135,17 @@ class Factorio(World):
|
||||
nauvis = Region("Nauvis", player, self.multiworld)
|
||||
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||
self.multiworld.evolution_traps[player] + \
|
||||
self.multiworld.attack_traps[player] + \
|
||||
self.multiworld.teleport_traps[player] + \
|
||||
self.multiworld.grenade_traps[player] + \
|
||||
self.multiworld.cluster_grenade_traps[player] + \
|
||||
self.multiworld.atomic_rocket_traps[player] + \
|
||||
self.multiworld.artillery_traps[player]
|
||||
self.options.evolution_traps + \
|
||||
self.options.attack_traps + \
|
||||
self.options.teleport_traps + \
|
||||
self.options.grenade_traps + \
|
||||
self.options.cluster_grenade_traps + \
|
||||
self.options.atomic_rocket_traps + \
|
||||
self.options.artillery_traps
|
||||
|
||||
location_pool = []
|
||||
|
||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||
location_pool.extend(location_pools[pack])
|
||||
try:
|
||||
location_names = self.multiworld.random.sample(location_pool, location_count)
|
||||
@@ -151,11 +154,11 @@ class Factorio(World):
|
||||
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
|
||||
f"or increase the location count (higher max science pack). (Player {self.player})") from e
|
||||
|
||||
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
|
||||
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis, self.options.tech_cost_mix)
|
||||
for loc_name in location_names]
|
||||
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
|
||||
min_cost = self.multiworld.min_tech_cost[self.player]
|
||||
max_cost = self.multiworld.max_tech_cost[self.player]
|
||||
distribution: TechCostDistribution = self.options.tech_cost_distribution
|
||||
min_cost = self.options.min_tech_cost
|
||||
max_cost = self.options.max_tech_cost
|
||||
if distribution == distribution.option_even:
|
||||
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
|
||||
else:
|
||||
@@ -164,7 +167,7 @@ class Factorio(World):
|
||||
distribution.option_high: max_cost}[distribution.value]
|
||||
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
|
||||
rand_values = sorted(rand_values)
|
||||
if self.multiworld.ramping_tech_costs[self.player]:
|
||||
if self.options.ramping_tech_costs:
|
||||
def sorter(loc: FactorioScienceLocation):
|
||||
return loc.complexity, loc.rel_cost
|
||||
else:
|
||||
@@ -179,7 +182,7 @@ class Factorio(World):
|
||||
event = FactorioItem("Victory", ItemClassification.progression, None, player)
|
||||
location.place_locked_item(event)
|
||||
|
||||
for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
|
||||
nauvis.locations.append(location)
|
||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||
@@ -195,10 +198,10 @@ class Factorio(World):
|
||||
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
|
||||
for trap_name in traps:
|
||||
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
|
||||
range(getattr(self.multiworld,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
|
||||
range(getattr(self.options,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
||||
|
||||
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
|
||||
want_progressives = collections.defaultdict(lambda: self.options.progressive.
|
||||
want_progressives(self.multiworld.random))
|
||||
|
||||
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
||||
@@ -206,7 +209,7 @@ class Factorio(World):
|
||||
"logistics": 1,
|
||||
"rocket-silo": -1}
|
||||
loc: FactorioScienceLocation
|
||||
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full:
|
||||
if self.options.tech_tree_information == TechTreeInformation.option_full:
|
||||
# mark all locations as pre-hinted
|
||||
for loc in self.science_locations:
|
||||
loc.revealed = True
|
||||
@@ -237,16 +240,17 @@ class Factorio(World):
|
||||
player = self.player
|
||||
shapes = get_shapes(self)
|
||||
|
||||
for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
|
||||
for ingredient in self.options.max_science_pack.get_allowed_packs():
|
||||
location = world.get_location(f"Automate {ingredient}", player)
|
||||
|
||||
if self.multiworld.recipe_ingredients[self.player]:
|
||||
if self.options.recipe_ingredients:
|
||||
custom_recipe = self.custom_recipes[ingredient]
|
||||
|
||||
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
|
||||
(ingredient not in technology_table or state.has(ingredient, player)) and \
|
||||
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
|
||||
for technology in required_technologies[sub_ingredient])
|
||||
for technology in required_technologies[sub_ingredient]) and \
|
||||
all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine])
|
||||
else:
|
||||
location.access_rule = lambda state, ingredient=ingredient: \
|
||||
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
|
||||
@@ -260,16 +264,16 @@ class Factorio(World):
|
||||
prerequisites: all(state.can_reach(loc) for loc in locations))
|
||||
|
||||
silo_recipe = None
|
||||
if self.multiworld.silo[self.player] == Silo.option_spawn:
|
||||
if self.options.silo == Silo.option_spawn:
|
||||
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
part_recipe = self.custom_recipes["rocket-part"]
|
||||
satellite_recipe = None
|
||||
if self.multiworld.goal[self.player] == Goal.option_satellite:
|
||||
if self.options.goal == Goal.option_satellite:
|
||||
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("satellite")))
|
||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
|
||||
if self.multiworld.silo[self.player] != Silo.option_spawn:
|
||||
if self.options.silo != Silo.option_spawn:
|
||||
victory_tech_names.add("rocket-silo")
|
||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||
for technology in
|
||||
@@ -278,12 +282,12 @@ class Factorio(World):
|
||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
|
||||
def generate_basic(self):
|
||||
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
|
||||
map_basic_settings = self.options.world_gen.value["basic"]
|
||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||
# 32 bit uint
|
||||
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)
|
||||
|
||||
start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value
|
||||
start_location_hints: typing.Set[str] = self.options.start_location_hints.value
|
||||
|
||||
for loc in self.science_locations:
|
||||
# show start_location_hints ingame
|
||||
@@ -307,8 +311,6 @@ class Factorio(World):
|
||||
|
||||
return super(Factorio, self).collect_item(state, item, remove)
|
||||
|
||||
option_definitions = factorio_options
|
||||
|
||||
@classmethod
|
||||
def stage_write_spoiler(cls, world, spoiler_handle):
|
||||
factorio_players = world.get_game_players(cls.game)
|
||||
@@ -436,25 +438,25 @@ class Factorio(World):
|
||||
|
||||
def set_custom_technologies(self):
|
||||
custom_technologies = {}
|
||||
allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs()
|
||||
allowed_packs = self.options.max_science_pack.get_allowed_packs()
|
||||
for technology_name, technology in base_technology_table.items():
|
||||
custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player)
|
||||
return custom_technologies
|
||||
|
||||
def set_custom_recipes(self):
|
||||
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
|
||||
ingredients_offset = self.options.recipe_ingredients_offset
|
||||
original_rocket_part = recipes["rocket-part"]
|
||||
science_pack_pools = get_science_pack_pools()
|
||||
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
||||
valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients)
|
||||
self.multiworld.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 + ingredients_offset)},
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
|
||||
if self.multiworld.recipe_ingredients[self.player]:
|
||||
if self.options.recipe_ingredients:
|
||||
valid_pool = []
|
||||
for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs():
|
||||
for pack in self.options.max_science_pack.get_ordered_science_packs():
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
self.multiworld.random.shuffle(valid_pool)
|
||||
if pack in recipes: # skips over space science pack
|
||||
@@ -462,23 +464,23 @@ class Factorio(World):
|
||||
ingredients_offset)
|
||||
self.custom_recipes[pack] = new_recipe
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
|
||||
or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||
if self.options.silo.value == Silo.option_randomize_recipe \
|
||||
or self.options.satellite.value == Satellite.option_randomize_recipe:
|
||||
valid_pool = set()
|
||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||
valid_pool |= science_pack_pools[pack]
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
|
||||
if self.options.silo.value == Silo.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(
|
||||
recipes["rocket-silo"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
||||
factor=(self.options.max_science_pack.value + 1) / 7,
|
||||
ingredients_offset=ingredients_offset)
|
||||
self.custom_recipes["rocket-silo"] = new_recipe
|
||||
|
||||
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||
if self.options.satellite.value == Satellite.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(
|
||||
recipes["satellite"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
||||
factor=(self.options.max_science_pack.value + 1) / 7,
|
||||
ingredients_offset=ingredients_offset)
|
||||
self.custom_recipes["satellite"] = new_recipe
|
||||
bridge = "ap-energy-bridge"
|
||||
@@ -486,16 +488,16 @@ class Factorio(World):
|
||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
|
||||
"replace_4": 1, "replace_5": 1, "replace_6": 1},
|
||||
{bridge: 1}, 10),
|
||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
|
||||
sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]),
|
||||
ingredients_offset=ingredients_offset)
|
||||
for ingredient_name in new_recipe.ingredients:
|
||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
|
||||
self.custom_recipes[bridge] = new_recipe
|
||||
|
||||
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
|
||||
if self.multiworld.silo[self.player] != Silo.option_spawn:
|
||||
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
|
||||
if self.options.silo != Silo.option_spawn:
|
||||
needed_recipes |= {"rocket-silo"}
|
||||
if self.multiworld.goal[self.player].value == Goal.option_satellite:
|
||||
if self.options.goal.value == Goal.option_satellite:
|
||||
needed_recipes |= {"satellite"}
|
||||
|
||||
for recipe in needed_recipes:
|
||||
@@ -537,7 +539,7 @@ class FactorioScienceLocation(FactorioLocation):
|
||||
ingredients: typing.Dict[str, int]
|
||||
count: int = 0
|
||||
|
||||
def __init__(self, player: int, name: str, address: int, parent: Region):
|
||||
def __init__(self, player: int, name: str, address: int, parent: Region, tech_cost_mix: TechCostMix):
|
||||
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
|
||||
# "AP-{Complexity}-{Cost}"
|
||||
self.complexity = int(self.name[3]) - 1
|
||||
@@ -545,7 +547,7 @@ class FactorioScienceLocation(FactorioLocation):
|
||||
|
||||
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
|
||||
for complexity in range(self.complexity):
|
||||
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
|
||||
if tech_cost_mix > parent.multiworld.random.randint(0, 99):
|
||||
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
|
||||
|
||||
@property
|
||||
|
||||
@@ -187,6 +187,7 @@ item_table = {
|
||||
"Pazuzu 5F": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 6F": ItemData(None, ItemClassification.progression),
|
||||
"Dark King": ItemData(None, ItemClassification.progression),
|
||||
"Tristam Bone Item Given": ItemData(None, ItemClassification.progression),
|
||||
#"Barred": ItemData(None, ItemClassification.progression),
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import Choice, FreeText, Toggle
|
||||
from Options import Choice, FreeText, Toggle, Range
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -131,6 +131,21 @@ class EnemizerAttacks(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class EnemizerGroups(Choice):
|
||||
"""Set which enemy groups will be affected by Enemizer."""
|
||||
display_name = "Enemizer Groups"
|
||||
option_mobs_only = 0
|
||||
option_mobs_and_bosses = 1
|
||||
option_mobs_bosses_and_dark_king = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class ShuffleResWeakType(Toggle):
|
||||
"""Resistance and Weakness types are shuffled for all enemies."""
|
||||
display_name = "Shuffle Resistance/Weakness Types"
|
||||
default = 0
|
||||
|
||||
|
||||
class ShuffleEnemiesPositions(Toggle):
|
||||
"""Instead of their original position in a given map, enemies are randomly placed."""
|
||||
display_name = "Shuffle Enemies' Positions"
|
||||
@@ -231,6 +246,81 @@ class BattlefieldsBattlesQuantities(Choice):
|
||||
option_random_one_through_ten = 6
|
||||
|
||||
|
||||
class CompanionLevelingType(Choice):
|
||||
"""Set how companions gain levels.
|
||||
Quests: Complete each companion's individual quest for them to promote to their second version.
|
||||
Quests Extended: Each companion has four exclusive quests, leveling each time a quest is completed.
|
||||
Save the Crystals (All): Each time a Crystal is saved, all companions gain levels.
|
||||
Save the Crystals (Individual): Each companion will level to their second version when a specific Crystal is saved.
|
||||
Benjamin Level: Companions' level tracks Benjamin's."""
|
||||
option_quests = 0
|
||||
option_quests_extended = 1
|
||||
option_save_crystals_individual = 2
|
||||
option_save_crystals_all = 3
|
||||
option_benjamin_level = 4
|
||||
option_benjamin_level_plus_5 = 5
|
||||
option_benjamin_level_plus_10 = 6
|
||||
default = 0
|
||||
display_name = "Companion Leveling Type"
|
||||
|
||||
|
||||
class CompanionSpellbookType(Choice):
|
||||
"""Update companions' spellbook.
|
||||
Standard: Original game spellbooks.
|
||||
Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
|
||||
Random Balanced: Randomize the spellbooks with an appropriate mix of spells.
|
||||
Random Chaos: Randomize the spellbooks in total free-for-all."""
|
||||
option_standard = 0
|
||||
option_extended = 1
|
||||
option_random_balanced = 2
|
||||
option_random_chaos = 3
|
||||
default = 0
|
||||
display_name = "Companion Spellbook Type"
|
||||
|
||||
|
||||
class StartingCompanion(Choice):
|
||||
"""Set a companion to start with.
|
||||
Random Companion: Randomly select one companion.
|
||||
Random Plus None: Randomly select a companion, with the possibility of none selected."""
|
||||
display_name = "Starting Companion"
|
||||
default = 0
|
||||
option_none = 0
|
||||
option_kaeli = 1
|
||||
option_tristam = 2
|
||||
option_phoebe = 3
|
||||
option_reuben = 4
|
||||
option_random_companion = 5
|
||||
option_random_plus_none = 6
|
||||
|
||||
|
||||
class AvailableCompanions(Range):
|
||||
"""Select randomly which companions will join your party. Unavailable companions can still be reached to get their items and complete their quests if needed.
|
||||
Note: If a Starting Companion is selected, it will always be available, regardless of this setting."""
|
||||
display_name = "Available Companions"
|
||||
default = 4
|
||||
range_start = 0
|
||||
range_end = 4
|
||||
|
||||
|
||||
class CompanionsLocations(Choice):
|
||||
"""Set the primary location of companions. Their secondary location is always the same.
|
||||
Standard: Companions will be at the same locations as in the original game.
|
||||
Shuffled: Companions' locations are shuffled amongst themselves.
|
||||
Shuffled Extended: Add all the Temples, as well as Phoebe's House and the Rope Bridge as possible locations."""
|
||||
display_name = "Companions' Locations"
|
||||
default = 0
|
||||
option_standard = 0
|
||||
option_shuffled = 1
|
||||
option_shuffled_extended = 2
|
||||
|
||||
|
||||
class KaelisMomFightsMinotaur(Toggle):
|
||||
"""Transfer Kaeli's requirements (Tree Wither, Elixir) and the two items she's giving to her mom.
|
||||
Kaeli will be available to join the party right away without the Tree Wither."""
|
||||
display_name = "Kaeli's Mom Fights Minotaur"
|
||||
default = 0
|
||||
|
||||
|
||||
option_definitions = {
|
||||
"logic": Logic,
|
||||
"brown_boxes": BrownBoxes,
|
||||
@@ -238,12 +328,21 @@ option_definitions = {
|
||||
"shattered_sky_coin_quantity": ShatteredSkyCoinQuantity,
|
||||
"starting_weapon": StartingWeapon,
|
||||
"progressive_gear": ProgressiveGear,
|
||||
"leveling_curve": LevelingCurve,
|
||||
"starting_companion": StartingCompanion,
|
||||
"available_companions": AvailableCompanions,
|
||||
"companions_locations": CompanionsLocations,
|
||||
"kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur,
|
||||
"companion_leveling_type": CompanionLevelingType,
|
||||
"companion_spellbook_type": CompanionSpellbookType,
|
||||
"enemies_density": EnemiesDensity,
|
||||
"enemies_scaling_lower": EnemiesScalingLower,
|
||||
"enemies_scaling_upper": EnemiesScalingUpper,
|
||||
"bosses_scaling_lower": BossesScalingLower,
|
||||
"bosses_scaling_upper": BossesScalingUpper,
|
||||
"enemizer_attacks": EnemizerAttacks,
|
||||
"enemizer_groups": EnemizerGroups,
|
||||
"shuffle_res_weak_types": ShuffleResWeakType,
|
||||
"shuffle_enemies_position": ShuffleEnemiesPositions,
|
||||
"progressive_formations": ProgressiveFormations,
|
||||
"doom_castle_mode": DoomCastle,
|
||||
@@ -253,6 +352,5 @@ option_definitions = {
|
||||
"crest_shuffle": CrestShuffle,
|
||||
"shuffle_battlefield_rewards": ShuffleBattlefieldRewards,
|
||||
"map_shuffle_seed": MapShuffleSeed,
|
||||
"leveling_curve": LevelingCurve,
|
||||
"battlefields_battles_quantities": BattlefieldsBattlesQuantities,
|
||||
}
|
||||
|
||||
@@ -35,46 +35,58 @@ def generate_output(self, output_directory):
|
||||
"item_name": location.item.name})
|
||||
|
||||
def cc(option):
|
||||
return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", "OverworldDungeons")
|
||||
return option.current_key.title().replace("_", "").replace("OverworldAndDungeons",
|
||||
"OverworldDungeons").replace("MobsAndBosses", "MobsBosses").replace("MobsBossesAndDarkKing",
|
||||
"MobsBossesDK").replace("BenjaminLevelPlus", "BenPlus").replace("BenjaminLevel", "BenPlus0").replace(
|
||||
"RandomCompanion", "Random")
|
||||
|
||||
def tf(option):
|
||||
return True if option else False
|
||||
|
||||
options = deepcopy(settings_template)
|
||||
options["name"] = self.multiworld.player_name[self.player]
|
||||
|
||||
option_writes = {
|
||||
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
|
||||
"chests_shuffle": "Include",
|
||||
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
|
||||
"npcs_shuffle": "Include",
|
||||
"battlefields_shuffle": "Include",
|
||||
"logic_options": cc(self.multiworld.logic[self.player]),
|
||||
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
|
||||
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
|
||||
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
|
||||
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
|
||||
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
|
||||
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
|
||||
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
|
||||
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
|
||||
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
|
||||
"RandomLow" if
|
||||
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
|
||||
"RandomHigh",
|
||||
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
|
||||
"random_starting_weapon": True,
|
||||
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
|
||||
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
|
||||
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
|
||||
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
|
||||
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
|
||||
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
|
||||
"enable_spoilers": False,
|
||||
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
|
||||
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
|
||||
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
|
||||
}
|
||||
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
|
||||
"chests_shuffle": "Include",
|
||||
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
|
||||
"npcs_shuffle": "Include",
|
||||
"battlefields_shuffle": "Include",
|
||||
"logic_options": cc(self.multiworld.logic[self.player]),
|
||||
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
|
||||
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
|
||||
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
|
||||
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
|
||||
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
|
||||
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
|
||||
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
|
||||
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
|
||||
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
|
||||
"RandomLow" if
|
||||
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
|
||||
"RandomHigh",
|
||||
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
|
||||
"random_starting_weapon": True,
|
||||
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
|
||||
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
|
||||
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
|
||||
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
|
||||
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
|
||||
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
|
||||
"enable_spoilers": False,
|
||||
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
|
||||
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
|
||||
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
|
||||
"enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]),
|
||||
"shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]),
|
||||
"companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]),
|
||||
"companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]),
|
||||
"starting_companion": cc(self.multiworld.starting_companion[self.player]),
|
||||
"available_companions": ["Zero", "One", "Two",
|
||||
"Three", "Four"][self.multiworld.available_companions[self.player].value],
|
||||
"companions_locations": cc(self.multiworld.companions_locations[self.player]),
|
||||
"kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]),
|
||||
}
|
||||
|
||||
for option, data in option_writes.items():
|
||||
options["Final Fantasy Mystic Quest"][option][data] = 1
|
||||
|
||||
@@ -83,7 +95,7 @@ def generate_output(self, output_directory):
|
||||
'utf8')
|
||||
self.rom_name_available_event.set()
|
||||
|
||||
setup = {"version": "1.4", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
|
||||
setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
|
||||
hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
|
||||
|
||||
starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
@@ -67,10 +67,10 @@ def create_regions(self):
|
||||
self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"],
|
||||
[FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in
|
||||
location_table else None, object["type"], object["access"],
|
||||
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for
|
||||
object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in
|
||||
("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or
|
||||
self.multiworld.brown_boxes[self.player] == "include")], room["links"]))
|
||||
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
|
||||
room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
|
||||
"BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and
|
||||
not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
|
||||
|
||||
dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
|
||||
dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", [])
|
||||
|
||||
@@ -108,8 +108,10 @@ class FFMQWorld(World):
|
||||
map_shuffle = multiworld.map_shuffle[world.player].value
|
||||
crest_shuffle = multiworld.crest_shuffle[world.player].current_key
|
||||
battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key
|
||||
companion_shuffle = multiworld.companions_locations[world.player].value
|
||||
kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key
|
||||
|
||||
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}"
|
||||
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
|
||||
|
||||
if query in rooms_data:
|
||||
world.rooms = rooms_data[query]
|
||||
|
||||
@@ -827,12 +827,12 @@
|
||||
id: 164
|
||||
area: 47
|
||||
coordinates: [14, 6]
|
||||
teleporter: [16, 2]
|
||||
teleporter: [98, 8] # Script for reuben, original value [16, 2]
|
||||
- name: Fireburg - Hotel
|
||||
id: 165
|
||||
area: 47
|
||||
coordinates: [20, 8]
|
||||
teleporter: [17, 2]
|
||||
teleporter: [96, 8] # It's a script now for tristam, original value [17, 2]
|
||||
- name: Fireburg - GrenadeMan House Script
|
||||
id: 166
|
||||
area: 47
|
||||
@@ -1178,6 +1178,16 @@
|
||||
area: 60
|
||||
coordinates: [2, 7]
|
||||
teleporter: [123, 0]
|
||||
- name: Lava Dome Pointless Room - Visit Quest Script 1
|
||||
id: 490
|
||||
area: 60
|
||||
coordinates: [4, 4]
|
||||
teleporter: [99, 8]
|
||||
- name: Lava Dome Pointless Room - Visit Quest Script 2
|
||||
id: 491
|
||||
area: 60
|
||||
coordinates: [4, 5]
|
||||
teleporter: [99, 8]
|
||||
- name: Lava Dome Lower Moon Helm Room - Left Entrance
|
||||
id: 235
|
||||
area: 60
|
||||
@@ -1568,6 +1578,11 @@
|
||||
area: 79
|
||||
coordinates: [2, 45]
|
||||
teleporter: [174, 0]
|
||||
- name: Mount Gale - Visit Quest
|
||||
id: 494
|
||||
area: 79
|
||||
coordinates: [44, 7]
|
||||
teleporter: [101, 8]
|
||||
- name: Windia - Main Entrance 1
|
||||
id: 312
|
||||
area: 80
|
||||
@@ -1613,11 +1628,11 @@
|
||||
area: 80
|
||||
coordinates: [21, 39]
|
||||
teleporter: [30, 5]
|
||||
- name: Windia - INN's Script # Change to teleporter
|
||||
- name: Windia - INN's Script # Change to teleporter / Change back to script!
|
||||
id: 321
|
||||
area: 80
|
||||
coordinates: [18, 34]
|
||||
teleporter: [31, 2] # Original value [79, 8]
|
||||
teleporter: [97, 8] # Original value [79, 8] > [31, 2]
|
||||
- name: Windia - Vendor House
|
||||
id: 322
|
||||
area: 80
|
||||
@@ -1697,7 +1712,7 @@
|
||||
id: 337
|
||||
area: 82
|
||||
coordinates: [45, 24]
|
||||
teleporter: [215, 0]
|
||||
teleporter: [102, 8] # Changed to script, original value [215, 0]
|
||||
- name: Windia Inn Lobby - Exit
|
||||
id: 338
|
||||
area: 82
|
||||
@@ -1998,6 +2013,16 @@
|
||||
area: 95
|
||||
coordinates: [29, 37]
|
||||
teleporter: [70, 8]
|
||||
- name: Light Temple - Visit Quest Script 1
|
||||
id: 492
|
||||
area: 95
|
||||
coordinates: [34, 39]
|
||||
teleporter: [100, 8]
|
||||
- name: Light Temple - Visit Quest Script 2
|
||||
id: 493
|
||||
area: 95
|
||||
coordinates: [35, 39]
|
||||
teleporter: [100, 8]
|
||||
- name: Ship Dock - Mobius Teleporter Script
|
||||
id: 397
|
||||
area: 96
|
||||
|
||||
@@ -309,13 +309,13 @@
|
||||
location: "WindiaBattlefield01"
|
||||
location_slot: "WindiaBattlefield01"
|
||||
type: "BattlefieldXp"
|
||||
access: []
|
||||
access: ["SandCoin", "RiverCoin"]
|
||||
- name: "South of Windia Battlefield"
|
||||
object_id: 0x14
|
||||
location: "WindiaBattlefield02"
|
||||
location_slot: "WindiaBattlefield02"
|
||||
type: "BattlefieldXp"
|
||||
access: []
|
||||
access: ["SandCoin", "RiverCoin"]
|
||||
links:
|
||||
- target_room: 9 # Focus Tower Windia
|
||||
location: "FocusTowerWindia"
|
||||
@@ -739,7 +739,7 @@
|
||||
object_id: 0x2E
|
||||
type: "Box"
|
||||
access: []
|
||||
- name: "Kaeli 1"
|
||||
- name: "Kaeli Companion"
|
||||
object_id: 0
|
||||
type: "Trigger"
|
||||
on_trigger: ["Kaeli1"]
|
||||
@@ -838,7 +838,7 @@
|
||||
- name: Sand Temple
|
||||
id: 24
|
||||
game_objects:
|
||||
- name: "Tristam Sand Temple"
|
||||
- name: "Tristam Companion"
|
||||
object_id: 0
|
||||
type: "Trigger"
|
||||
on_trigger: ["Tristam"]
|
||||
@@ -883,6 +883,11 @@
|
||||
object_id: 2
|
||||
type: "NPC"
|
||||
access: ["Tristam"]
|
||||
- name: "Tristam Bone Dungeon Item Given"
|
||||
object_id: 0
|
||||
type: "Trigger"
|
||||
on_trigger: ["TristamBoneItemGiven"]
|
||||
access: ["Tristam"]
|
||||
links:
|
||||
- target_room: 25
|
||||
entrance: 59
|
||||
@@ -1080,7 +1085,7 @@
|
||||
object_id: 0x40
|
||||
type: "Box"
|
||||
access: []
|
||||
- name: "Phoebe"
|
||||
- name: "Phoebe Companion"
|
||||
object_id: 0
|
||||
type: "Trigger"
|
||||
on_trigger: ["Phoebe1"]
|
||||
@@ -1846,11 +1851,11 @@
|
||||
access: []
|
||||
- target_room: 77
|
||||
entrance: 164
|
||||
teleporter: [16, 2]
|
||||
teleporter: [98, 8] # original value [16, 2]
|
||||
access: []
|
||||
- target_room: 82
|
||||
entrance: 165
|
||||
teleporter: [17, 2]
|
||||
teleporter: [96, 8] # original value [17, 2]
|
||||
access: []
|
||||
- target_room: 208
|
||||
access: ["Claw"]
|
||||
@@ -1875,7 +1880,7 @@
|
||||
object_id: 14
|
||||
type: "NPC"
|
||||
access: ["ReubenDadSaved"]
|
||||
- name: "Reuben"
|
||||
- name: "Reuben Companion"
|
||||
object_id: 0
|
||||
type: "Trigger"
|
||||
on_trigger: ["Reuben1"]
|
||||
@@ -1951,12 +1956,7 @@
|
||||
- name: "Fireburg - Tristam"
|
||||
object_id: 10
|
||||
type: "NPC"
|
||||
access: []
|
||||
- name: "Tristam Fireburg"
|
||||
object_id: 0
|
||||
type: "Trigger"
|
||||
on_trigger: ["Tristam"]
|
||||
access: []
|
||||
access: ["Tristam", "TristamBoneItemGiven"]
|
||||
links:
|
||||
- target_room: 76
|
||||
entrance: 177
|
||||
@@ -3183,7 +3183,7 @@
|
||||
access: []
|
||||
- target_room: 163
|
||||
entrance: 321
|
||||
teleporter: [31, 2]
|
||||
teleporter: [97, 8]
|
||||
access: []
|
||||
- target_room: 165
|
||||
entrance: 322
|
||||
@@ -3292,7 +3292,7 @@
|
||||
access: []
|
||||
- target_room: 164
|
||||
entrance: 337
|
||||
teleporter: [215, 0]
|
||||
teleporter: [102, 8]
|
||||
access: []
|
||||
- name: Windia Inn Beds
|
||||
id: 164
|
||||
|
||||
@@ -73,14 +73,57 @@ Final Fantasy Mystic Quest:
|
||||
Chaos: 0
|
||||
SelfDestruct: 0
|
||||
SimpleShuffle: 0
|
||||
enemizer_groups:
|
||||
MobsOnly: 0
|
||||
MobsBosses: 0
|
||||
MobsBossesDK: 0
|
||||
shuffle_res_weak_type:
|
||||
true: 0
|
||||
false: 0
|
||||
leveling_curve:
|
||||
Half: 0
|
||||
Normal: 0
|
||||
OneAndHalf: 0
|
||||
Double: 0
|
||||
DoubleHalf: 0
|
||||
DoubleAndHalf: 0
|
||||
Triple: 0
|
||||
Quadruple: 0
|
||||
companion_leveling_type:
|
||||
Quests: 0
|
||||
QuestsExtended: 0
|
||||
SaveCrystalsIndividual: 0
|
||||
SaveCrystalsAll: 0
|
||||
BenPlus0: 0
|
||||
BenPlus5: 0
|
||||
BenPlus10: 0
|
||||
companion_spellbook_type:
|
||||
Standard: 0
|
||||
Extended: 0
|
||||
RandomBalanced: 0
|
||||
RandomChaos: 0
|
||||
starting_companion:
|
||||
None: 0
|
||||
Kaeli: 0
|
||||
Tristam: 0
|
||||
Phoebe: 0
|
||||
Reuben: 0
|
||||
Random: 0
|
||||
RandomPlusNone: 0
|
||||
available_companions:
|
||||
Zero: 0
|
||||
One: 0
|
||||
Two: 0
|
||||
Three: 0
|
||||
Four: 0
|
||||
Random14: 0
|
||||
Random04: 0
|
||||
companions_locations:
|
||||
Standard: 0
|
||||
Shuffled: 0
|
||||
ShuffledExtended: 0
|
||||
kaelis_mom_fight_minotaur:
|
||||
true: 0
|
||||
false: 0
|
||||
battles_quantity:
|
||||
Ten: 0
|
||||
Seven: 0
|
||||
|
||||
@@ -15,9 +15,9 @@ else:
|
||||
|
||||
def locality_needed(world: MultiWorld) -> bool:
|
||||
for player in world.player_ids:
|
||||
if world.local_items[player].value:
|
||||
if world.worlds[player].options.local_items.value:
|
||||
return True
|
||||
if world.non_local_items[player].value:
|
||||
if world.worlds[player].options.non_local_items.value:
|
||||
return True
|
||||
|
||||
# Group
|
||||
@@ -40,12 +40,12 @@ def locality_rules(world: MultiWorld):
|
||||
forbid_data[sender][receiver].update(items)
|
||||
|
||||
for receiving_player in world.player_ids:
|
||||
local_items: typing.Set[str] = world.local_items[receiving_player].value
|
||||
local_items: typing.Set[str] = world.worlds[receiving_player].options.local_items.value
|
||||
if local_items:
|
||||
for sending_player in world.player_ids:
|
||||
if receiving_player != sending_player:
|
||||
forbid(sending_player, receiving_player, local_items)
|
||||
non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value
|
||||
non_local_items: typing.Set[str] = world.worlds[receiving_player].options.non_local_items.value
|
||||
if non_local_items:
|
||||
forbid(receiving_player, receiving_player, non_local_items)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import typing
|
||||
from .ExtractedData import logic_options, starts, pool_options
|
||||
from .Rules import cost_terms
|
||||
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink
|
||||
from .Charms import vanilla_costs, names as charm_names
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -402,22 +402,34 @@ class WhitePalace(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class DeathLink(Choice):
|
||||
class ExtraPlatforms(DefaultOnToggle):
|
||||
"""Places additional platforms to make traveling throughout Hallownest more convenient."""
|
||||
|
||||
|
||||
class DeathLinkShade(Choice):
|
||||
"""Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any.
|
||||
|
||||
vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any.
|
||||
shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched.
|
||||
shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless.
|
||||
|
||||
* This option has no effect if DeathLink is disabled.
|
||||
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
|
||||
your existing shade, if any.
|
||||
"""
|
||||
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_no = 0
|
||||
alias_true = 1
|
||||
alias_on = 1
|
||||
alias_yes = 1
|
||||
option_vanilla = 0
|
||||
option_shadeless = 1
|
||||
option_vanilla = 2
|
||||
option_shade = 3
|
||||
option_shade = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class DeathLinkBreaksFragileCharms(Toggle):
|
||||
"""Sets if fragile charms break when you are killed by a DeathLink.
|
||||
|
||||
* This option has no effect if DeathLink is disabled.
|
||||
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
|
||||
will continue to do so.
|
||||
"""
|
||||
|
||||
|
||||
class StartingGeo(Range):
|
||||
@@ -476,7 +488,8 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
**{
|
||||
option.__name__: option
|
||||
for option in (
|
||||
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
|
||||
StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo,
|
||||
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
|
||||
MinimumGeoPrice, MaximumGeoPrice,
|
||||
MinimumGrubPrice, MaximumGrubPrice,
|
||||
MinimumEssencePrice, MaximumEssencePrice,
|
||||
@@ -488,7 +501,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
LegEaterShopSlots, GrubfatherRewardSlots,
|
||||
SeerRewardSlots, ExtraShopSlots,
|
||||
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
|
||||
CostSanity, CostSanityHybridChance,
|
||||
CostSanity, CostSanityHybridChance
|
||||
)
|
||||
},
|
||||
**cost_sanity_weights
|
||||
|
||||
@@ -419,17 +419,16 @@ class HKWorld(World):
|
||||
def set_rules(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
if world.logic[player] != 'nologic':
|
||||
goal = world.Goal[player]
|
||||
if goal == Goal.option_hollowknight:
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
|
||||
elif goal == Goal.option_siblings:
|
||||
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
|
||||
elif goal == Goal.option_radiance:
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
|
||||
else:
|
||||
# Any goal
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
|
||||
goal = world.Goal[player]
|
||||
if goal == Goal.option_hollowknight:
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
|
||||
elif goal == Goal.option_siblings:
|
||||
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
|
||||
elif goal == Goal.option_radiance:
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
|
||||
else:
|
||||
# Any goal
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
|
||||
|
||||
set_rules(self)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class KH2Context(CommonContext):
|
||||
self.kh2_local_items = None
|
||||
self.growthlevel = None
|
||||
self.kh2connected = False
|
||||
self.kh2_finished_game = False
|
||||
self.serverconneced = False
|
||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
@@ -833,9 +834,9 @@ async def kh2_watcher(ctx: KH2Context):
|
||||
await asyncio.create_task(ctx.verifyItems())
|
||||
await asyncio.create_task(ctx.verifyLevel())
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
if finishedGame(ctx, message):
|
||||
if finishedGame(ctx, message) and not ctx.kh2_finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
ctx.kh2_finished_game = True
|
||||
await ctx.send_msgs(message)
|
||||
elif not ctx.kh2connected and ctx.serverconneced:
|
||||
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
|
||||
|
||||
@@ -1020,10 +1020,9 @@ def create_regions(self):
|
||||
multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in
|
||||
KH2REGIONS.items()]
|
||||
# fill the event locations with events
|
||||
multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table})
|
||||
for location, item in event_location_to_item.items():
|
||||
multiworld.get_location(location, player).place_locked_item(
|
||||
multiworld.worlds[player].create_item(item))
|
||||
multiworld.worlds[player].create_event_item(item))
|
||||
|
||||
|
||||
def connect_regions(self):
|
||||
|
||||
@@ -224,7 +224,7 @@ class KH2WorldRules(KH2Rules):
|
||||
RegionName.Pl2: lambda state: self.pl_unlocked(state, 2),
|
||||
|
||||
RegionName.Ag: lambda state: self.ag_unlocked(state, 1),
|
||||
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2),
|
||||
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state),
|
||||
|
||||
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
|
||||
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
|
||||
|
||||
@@ -119,11 +119,15 @@ class KH2World(World):
|
||||
item_classification = ItemClassification.useful
|
||||
else:
|
||||
item_classification = ItemClassification.filler
|
||||
|
||||
created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player)
|
||||
|
||||
return created_item
|
||||
|
||||
def create_event_item(self, name: str) -> Item:
|
||||
item_classification = ItemClassification.progression
|
||||
created_item = KH2Item(name, item_classification, None, self.player)
|
||||
return created_item
|
||||
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking.
|
||||
@@ -461,7 +465,7 @@ class KH2World(World):
|
||||
if location in self.random_super_boss_list:
|
||||
self.random_super_boss_list.remove(location)
|
||||
|
||||
if not self.options.SummonLevelLocationToggle:
|
||||
if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list:
|
||||
self.random_super_boss_list.remove(LocationName.Summonlvl7)
|
||||
|
||||
# Testing if the player has the right amount of Bounties for Completion.
|
||||
|
||||
@@ -349,18 +349,19 @@ class GfxMod(FreeText, LADXROption):
|
||||
normal = ''
|
||||
default = 'Link'
|
||||
|
||||
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx'))
|
||||
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
|
||||
__spriteDir: str = None
|
||||
|
||||
extensions = [".bin", ".bdiff", ".png", ".bmp"]
|
||||
|
||||
for file in os.listdir(__spriteDir):
|
||||
name, extension = os.path.splitext(file)
|
||||
if extension in extensions:
|
||||
__spriteFiles[name].append(file)
|
||||
|
||||
def __init__(self, value: str):
|
||||
super().__init__(value)
|
||||
if not GfxMod.__spriteDir:
|
||||
GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx'))
|
||||
for file in os.listdir(GfxMod.__spriteDir):
|
||||
name, extension = os.path.splitext(file)
|
||||
if extension in self.extensions:
|
||||
GfxMod.__spriteFiles[name].append(file)
|
||||
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if self.value == "Link" or self.value in GfxMod.__spriteFiles:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Archipelago init file for Lingo
|
||||
"""
|
||||
from logging import warning
|
||||
|
||||
from BaseClasses import Item, ItemClassification, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .items import ALL_ITEM_TABLE, LingoItem
|
||||
@@ -9,7 +11,6 @@ from .options import LingoOptions
|
||||
from .player_logic import LingoPlayerLogic
|
||||
from .regions import create_regions
|
||||
from .static_logic import Room, RoomEntrance
|
||||
from .testing import LingoTestOptions
|
||||
|
||||
|
||||
class LingoWebWorld(WebWorld):
|
||||
@@ -49,6 +50,14 @@ class LingoWorld(World):
|
||||
player_logic: LingoPlayerLogic
|
||||
|
||||
def generate_early(self):
|
||||
if not (self.options.shuffle_doors or self.options.shuffle_colors):
|
||||
if self.multiworld.players == 1:
|
||||
warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression"
|
||||
f" items. Please turn on Door Shuffle or Color Shuffle if that doesn't seem right.")
|
||||
else:
|
||||
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any"
|
||||
f" progression items. Please turn on Door Shuffle or Color Shuffle.")
|
||||
|
||||
self.player_logic = LingoPlayerLogic(self)
|
||||
|
||||
def create_regions(self):
|
||||
@@ -94,9 +103,11 @@ class LingoWorld(World):
|
||||
classification = item.classification
|
||||
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\
|
||||
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping
|
||||
for painting_id in item.painting_ids):
|
||||
for painting_id in item.painting_ids)\
|
||||
and "pilgrim_painting2" not in item.painting_ids:
|
||||
# If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings
|
||||
# go nowhere, then this item should not be progression.
|
||||
# go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be
|
||||
# excluded from this.
|
||||
classification = ItemClassification.filler
|
||||
|
||||
return LingoItem(name, classification, item.code, self.player)
|
||||
|
||||
@@ -373,6 +373,7 @@
|
||||
ANOTHER TRY:
|
||||
id: Entry Room/Panel_advance
|
||||
tag: topwhite
|
||||
non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt.
|
||||
LEVEL 2:
|
||||
# We will set up special rules for this in code.
|
||||
id: EndPanel/Panel_level_2
|
||||
@@ -1033,6 +1034,8 @@
|
||||
Hallway Room (3): True
|
||||
Hallway Room (4): True
|
||||
Hedge Maze: True # through the door to the sectioned-off part of the hedge maze
|
||||
Cellar:
|
||||
door: Lookout Entrance
|
||||
panels:
|
||||
MASSACRED:
|
||||
id: Palindrome Room/Panel_massacred_sacred
|
||||
@@ -1168,11 +1171,21 @@
|
||||
- KEEP
|
||||
- BAILEY
|
||||
- TOWER
|
||||
Lookout Entrance:
|
||||
id: Cross Room Doors/Door_missing
|
||||
location_name: Outside The Agreeable - Lookout Panels
|
||||
panels:
|
||||
- NORTH
|
||||
- WINTER
|
||||
- DIAMONDS
|
||||
- FIRE
|
||||
paintings:
|
||||
- id: panda_painting
|
||||
orientation: south
|
||||
- id: eyes_yellow_painting
|
||||
orientation: east
|
||||
- id: pencil_painting7
|
||||
orientation: north
|
||||
progression:
|
||||
Progressive Hallway Room:
|
||||
- Hallway Door
|
||||
@@ -2043,7 +2056,7 @@
|
||||
door: Sixth Floor
|
||||
Cellar:
|
||||
room: Room Room
|
||||
door: Shortcut to Fifth Floor
|
||||
door: Cellar Exit
|
||||
Welcome Back Area:
|
||||
door: Welcome Back
|
||||
Art Gallery:
|
||||
@@ -2302,9 +2315,6 @@
|
||||
id: Master Room/Panel_mastery_mastery3
|
||||
tag: midwhite
|
||||
hunt: True
|
||||
required_door:
|
||||
room: Orange Tower Seventh Floor
|
||||
door: Mastery
|
||||
THE LIBRARY:
|
||||
id: EndPanel/Panel_library
|
||||
check: True
|
||||
@@ -2675,6 +2685,10 @@
|
||||
Outside The Undeterred: True
|
||||
Outside The Agreeable: True
|
||||
Outside The Wanderer: True
|
||||
The Observant: True
|
||||
Art Gallery: True
|
||||
The Scientific: True
|
||||
Cellar: True
|
||||
Orange Tower Fifth Floor:
|
||||
room: Orange Tower Fifth Floor
|
||||
door: Welcome Back
|
||||
@@ -2991,8 +3005,7 @@
|
||||
PATS:
|
||||
id: Rhyme Room/Panel_wrath_path
|
||||
colors: purple
|
||||
tag: midpurp and rhyme
|
||||
copy_to_sign: sign15
|
||||
tag: forbid
|
||||
KNIGHT:
|
||||
id: Rhyme Room/Panel_knight_write
|
||||
colors: purple
|
||||
@@ -3158,6 +3171,8 @@
|
||||
door: Painting Shortcut
|
||||
painting: True
|
||||
Room Room: True # trapdoor
|
||||
Outside The Agreeable:
|
||||
painting: True
|
||||
panels:
|
||||
UNOPEN:
|
||||
id: Truncate Room/Panel_unopened_open
|
||||
@@ -6299,17 +6314,22 @@
|
||||
SKELETON:
|
||||
id: Double Room/Panel_bones_syn
|
||||
tag: syn rhyme
|
||||
colors: purple
|
||||
subtag: bot
|
||||
link: rhyme BONES
|
||||
REPENTANCE:
|
||||
id: Double Room/Panel_sentence_rhyme
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- blue
|
||||
tag: whole rhyme
|
||||
subtag: top
|
||||
link: rhyme SENTENCE
|
||||
WORD:
|
||||
id: Double Room/Panel_sentence_whole
|
||||
colors: blue
|
||||
colors:
|
||||
- purple
|
||||
- blue
|
||||
tag: whole rhyme
|
||||
subtag: bot
|
||||
link: rhyme SENTENCE
|
||||
@@ -6321,6 +6341,7 @@
|
||||
link: rhyme DREAM
|
||||
FANTASY:
|
||||
id: Double Room/Panel_dream_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme DREAM
|
||||
@@ -6332,6 +6353,7 @@
|
||||
link: rhyme MYSTERY
|
||||
SECRET:
|
||||
id: Double Room/Panel_mystery_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme MYSTERY
|
||||
@@ -6386,25 +6408,33 @@
|
||||
door: Nines
|
||||
FERN:
|
||||
id: Double Room/Panel_return_rhyme
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- black
|
||||
tag: ant rhyme
|
||||
subtag: top
|
||||
link: rhyme RETURN
|
||||
STAY:
|
||||
id: Double Room/Panel_return_ant
|
||||
colors: black
|
||||
colors:
|
||||
- purple
|
||||
- black
|
||||
tag: ant rhyme
|
||||
subtag: bot
|
||||
link: rhyme RETURN
|
||||
FRIEND:
|
||||
id: Double Room/Panel_descend_rhyme
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- black
|
||||
tag: ant rhyme
|
||||
subtag: top
|
||||
link: rhyme DESCEND
|
||||
RISE:
|
||||
id: Double Room/Panel_descend_ant
|
||||
colors: black
|
||||
colors:
|
||||
- purple
|
||||
- black
|
||||
tag: ant rhyme
|
||||
subtag: bot
|
||||
link: rhyme DESCEND
|
||||
@@ -6416,6 +6446,7 @@
|
||||
link: rhyme JUMP
|
||||
BOUNCE:
|
||||
id: Double Room/Panel_jump_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme JUMP
|
||||
@@ -6427,6 +6458,7 @@
|
||||
link: rhyme FALL
|
||||
PLUNGE:
|
||||
id: Double Room/Panel_fall_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme FALL
|
||||
@@ -6456,13 +6488,17 @@
|
||||
panels:
|
||||
BIRD:
|
||||
id: Double Room/Panel_word_rhyme
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- blue
|
||||
tag: whole rhyme
|
||||
subtag: top
|
||||
link: rhyme WORD
|
||||
LETTER:
|
||||
id: Double Room/Panel_word_whole
|
||||
colors: blue
|
||||
colors:
|
||||
- purple
|
||||
- blue
|
||||
tag: whole rhyme
|
||||
subtag: bot
|
||||
link: rhyme WORD
|
||||
@@ -6474,6 +6510,7 @@
|
||||
link: rhyme HIDDEN
|
||||
CONCEALED:
|
||||
id: Double Room/Panel_hidden_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme HIDDEN
|
||||
@@ -6485,6 +6522,7 @@
|
||||
link: rhyme SILENT
|
||||
MUTE:
|
||||
id: Double Room/Panel_silent_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme SILENT
|
||||
@@ -6531,6 +6569,7 @@
|
||||
link: rhyme BLOCKED
|
||||
OBSTRUCTED:
|
||||
id: Double Room/Panel_blocked_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme BLOCKED
|
||||
@@ -6542,6 +6581,7 @@
|
||||
link: rhyme RISE
|
||||
SWELL:
|
||||
id: Double Room/Panel_rise_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme RISE
|
||||
@@ -6553,6 +6593,7 @@
|
||||
link: rhyme ASCEND
|
||||
CLIMB:
|
||||
id: Double Room/Panel_ascend_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme ASCEND
|
||||
@@ -6564,6 +6605,7 @@
|
||||
link: rhyme DOUBLE
|
||||
DUPLICATE:
|
||||
id: Double Room/Panel_double_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme DOUBLE
|
||||
@@ -6642,6 +6684,7 @@
|
||||
link: rhyme CHILD
|
||||
KID:
|
||||
id: Double Room/Panel_child_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme CHILD
|
||||
@@ -6653,6 +6696,7 @@
|
||||
link: rhyme CRYSTAL
|
||||
QUARTZ:
|
||||
id: Double Room/Panel_crystal_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme CRYSTAL
|
||||
@@ -6664,6 +6708,7 @@
|
||||
link: rhyme CREATIVE
|
||||
INNOVATIVE (Bottom):
|
||||
id: Double Room/Panel_creative_syn
|
||||
colors: purple
|
||||
tag: syn rhyme
|
||||
subtag: bot
|
||||
link: rhyme CREATIVE
|
||||
@@ -6882,7 +6927,7 @@
|
||||
event: True
|
||||
panels:
|
||||
- WALL (1)
|
||||
Shortcut to Fifth Floor:
|
||||
Cellar Exit:
|
||||
id:
|
||||
- Tower Room Area Doors/Door_panel_basement
|
||||
- Tower Room Area Doors/Door_panel_basement2
|
||||
@@ -6895,7 +6940,10 @@
|
||||
door: Excavation
|
||||
Orange Tower Fifth Floor:
|
||||
room: Room Room
|
||||
door: Shortcut to Fifth Floor
|
||||
door: Cellar Exit
|
||||
Outside The Agreeable:
|
||||
room: Outside The Agreeable
|
||||
door: Lookout Entrance
|
||||
Outside The Wise:
|
||||
entrances:
|
||||
Orange Tower Sixth Floor:
|
||||
@@ -7319,49 +7367,65 @@
|
||||
link: change GRAVITY
|
||||
PART:
|
||||
id: Chemistry Room/Panel_physics_2
|
||||
colors: blue
|
||||
colors:
|
||||
- blue
|
||||
- red
|
||||
tag: blue mid red bot
|
||||
subtag: mid
|
||||
link: xur PARTICLE
|
||||
MATTER:
|
||||
id: Chemistry Room/Panel_physics_1
|
||||
colors: red
|
||||
colors:
|
||||
- blue
|
||||
- red
|
||||
tag: blue mid red bot
|
||||
subtag: bot
|
||||
link: xur PARTICLE
|
||||
ELECTRIC:
|
||||
id: Chemistry Room/Panel_physics_6
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- red
|
||||
tag: purple mid red bot
|
||||
subtag: mid
|
||||
link: xpr ELECTRON
|
||||
ATOM (1):
|
||||
id: Chemistry Room/Panel_physics_3
|
||||
colors: red
|
||||
colors:
|
||||
- purple
|
||||
- red
|
||||
tag: purple mid red bot
|
||||
subtag: bot
|
||||
link: xpr ELECTRON
|
||||
NEUTRAL:
|
||||
id: Chemistry Room/Panel_physics_7
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- red
|
||||
tag: purple mid red bot
|
||||
subtag: mid
|
||||
link: xpr NEUTRON
|
||||
ATOM (2):
|
||||
id: Chemistry Room/Panel_physics_4
|
||||
colors: red
|
||||
colors:
|
||||
- purple
|
||||
- red
|
||||
tag: purple mid red bot
|
||||
subtag: bot
|
||||
link: xpr NEUTRON
|
||||
PROPEL:
|
||||
id: Chemistry Room/Panel_physics_8
|
||||
colors: purple
|
||||
colors:
|
||||
- purple
|
||||
- red
|
||||
tag: purple mid red bot
|
||||
subtag: mid
|
||||
link: xpr PROTON
|
||||
ATOM (3):
|
||||
id: Chemistry Room/Panel_physics_5
|
||||
colors: red
|
||||
colors:
|
||||
- purple
|
||||
- red
|
||||
tag: purple mid red bot
|
||||
subtag: bot
|
||||
link: xpr PROTON
|
||||
|
||||
@@ -1064,6 +1064,9 @@ doors:
|
||||
Hallway Door:
|
||||
item: 444459
|
||||
location: 445214
|
||||
Lookout Entrance:
|
||||
item: 444579
|
||||
location: 445271
|
||||
Dread Hallway:
|
||||
Tenacious Entrance:
|
||||
item: 444462
|
||||
@@ -1402,7 +1405,7 @@ doors:
|
||||
item: 444570
|
||||
location: 445266
|
||||
Room Room:
|
||||
Shortcut to Fifth Floor:
|
||||
Cellar Exit:
|
||||
item: 444571
|
||||
location: 445076
|
||||
Outside The Wise:
|
||||
|
||||
@@ -32,7 +32,7 @@ class LocationChecks(Choice):
|
||||
option_insanity = 2
|
||||
|
||||
|
||||
class ShuffleColors(Toggle):
|
||||
class ShuffleColors(DefaultOnToggle):
|
||||
"""If on, an item is added to the pool for every puzzle color (besides White).
|
||||
You will need to unlock the requisite colors in order to be able to solve puzzles of that color."""
|
||||
display_name = "Shuffle Colors"
|
||||
|
||||
@@ -6,7 +6,6 @@ from .options import LocationChecks, ShuffleDoors, VictoryCondition
|
||||
from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
|
||||
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \
|
||||
RoomAndPanel
|
||||
from .testing import LingoTestOptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
@@ -190,6 +189,25 @@ class LingoPlayerLogic:
|
||||
if item.should_include(world):
|
||||
self.real_items.append(name)
|
||||
|
||||
# Calculate the requirements for the fake pilgrimage.
|
||||
fake_pilgrimage = [
|
||||
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
|
||||
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
|
||||
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
|
||||
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
|
||||
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
|
||||
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
|
||||
["Outside The Agreeable", "Tenacious Entrance"]
|
||||
]
|
||||
pilgrimage_reqs = AccessRequirements()
|
||||
for door in fake_pilgrimage:
|
||||
door_object = DOORS_BY_ROOM[door[0]][door[1]]
|
||||
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
|
||||
pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world))
|
||||
else:
|
||||
pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1]))
|
||||
self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs
|
||||
|
||||
# Create the paintings mapping, if painting shuffle is on.
|
||||
if painting_shuffle:
|
||||
# Shuffle paintings until we get something workable.
|
||||
@@ -205,7 +223,7 @@ class LingoPlayerLogic:
|
||||
"kind of logic error.")
|
||||
|
||||
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
|
||||
and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False:
|
||||
and not early_color_hallways is False:
|
||||
# If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
|
||||
# but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
|
||||
# now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are
|
||||
@@ -369,11 +387,9 @@ class LingoPlayerLogic:
|
||||
door_object = DOORS_BY_ROOM[room][door]
|
||||
|
||||
for req_panel in door_object.panels:
|
||||
if req_panel.room is not None and req_panel.room != room:
|
||||
access_reqs.rooms.add(req_panel.room)
|
||||
|
||||
sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
|
||||
req_panel.panel, world)
|
||||
panel_room = room if req_panel.room is None else req_panel.room
|
||||
access_reqs.rooms.add(panel_room)
|
||||
sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world)
|
||||
access_reqs.merge(sub_access_reqs)
|
||||
|
||||
self.door_reqs[room][door] = access_reqs
|
||||
@@ -397,8 +413,8 @@ class LingoPlayerLogic:
|
||||
unhindered_panels_by_color: dict[Optional[str], int] = {}
|
||||
|
||||
for panel_name, panel_data in room_data.items():
|
||||
# We won't count non-counting panels.
|
||||
if panel_data.non_counting:
|
||||
# We won't count non-counting panels. THE MASTER has special access rules and is handled separately.
|
||||
if panel_data.non_counting or panel_name == "THE MASTER":
|
||||
continue
|
||||
|
||||
# We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
|
||||
|
||||
@@ -4,7 +4,7 @@ from BaseClasses import Entrance, ItemClassification, Region
|
||||
from .items import LingoItem
|
||||
from .locations import LingoLocation
|
||||
from .player_logic import LingoPlayerLogic
|
||||
from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda
|
||||
from .rules import lingo_can_use_entrance, make_location_lambda
|
||||
from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -25,15 +25,6 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi
|
||||
return new_region
|
||||
|
||||
|
||||
def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
target_region = regions["Pilgrim Antechamber"]
|
||||
source_region = regions["Outside The Agreeable"]
|
||||
source_region.connect(
|
||||
target_region,
|
||||
"Pilgrimage",
|
||||
lambda state: lingo_can_use_pilgrimage(state, world, player_logic))
|
||||
|
||||
|
||||
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
|
||||
door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
connection = Entrance(world.player, description, source_region)
|
||||
@@ -91,7 +82,9 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
|
||||
player_logic)
|
||||
|
||||
handle_pilgrim_room(regions, world, player_logic)
|
||||
# Add the fake pilgrimage.
|
||||
connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage",
|
||||
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic)
|
||||
|
||||
if early_color_hallways:
|
||||
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
|
||||
|
||||
@@ -17,23 +17,6 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor,
|
||||
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
|
||||
|
||||
|
||||
def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
fake_pilgrimage = [
|
||||
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
|
||||
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
|
||||
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
|
||||
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
|
||||
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
|
||||
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
|
||||
["Outside The Agreeable", "Tenacious Entrance"]
|
||||
]
|
||||
for entrance in fake_pilgrimage:
|
||||
if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
|
||||
@@ -56,6 +39,12 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld",
|
||||
counted_panels += panel_count
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
# THE MASTER has to be handled separately, because it has special access rules.
|
||||
if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
|
||||
and lingo_can_use_mastery_location(state, world, player_logic):
|
||||
counted_panels += 1
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,13 @@ from . import LingoTestBase
|
||||
|
||||
class TestRequiredRoomLogic(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_pilgrim_first(self) -> None:
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
@@ -27,6 +30,8 @@ class TestRequiredRoomLogic(LingoTestBase):
|
||||
self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
def test_hidden_first(self) -> None:
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
@@ -49,10 +54,13 @@ class TestRequiredRoomLogic(LingoTestBase):
|
||||
|
||||
class TestRequiredDoorLogic(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_through_rhyme(self) -> None:
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Starting Room - Rhyme Room Entrance")
|
||||
@@ -62,6 +70,8 @@ class TestRequiredDoorLogic(LingoTestBase):
|
||||
self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
def test_through_hidden(self) -> None:
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Starting Room - Rhyme Room Entrance")
|
||||
@@ -76,10 +86,13 @@ class TestRequiredDoorLogic(LingoTestBase):
|
||||
|
||||
class TestSimpleDoors(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "simple"
|
||||
"shuffle_doors": "simple",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ class TestProgressiveOrangeTower(LingoTestBase):
|
||||
}
|
||||
|
||||
def test_from_welcome_back(self) -> None:
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
@@ -83,6 +85,8 @@ class TestProgressiveOrangeTower(LingoTestBase):
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
def test_from_hub_room(self) -> None:
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
|
||||
@@ -7,6 +7,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase):
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
@@ -58,6 +60,8 @@ class TestSimpleHallwayRoom(LingoTestBase):
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
@@ -81,10 +85,13 @@ class TestSimpleHallwayRoom(LingoTestBase):
|
||||
|
||||
class TestProgressiveArtGallery(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.remove_forced_good_item()
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from test.bases import WorldTestBase
|
||||
from .. import LingoTestOptions
|
||||
|
||||
|
||||
class LingoTestBase(WorldTestBase):
|
||||
@@ -9,5 +8,10 @@ class LingoTestBase(WorldTestBase):
|
||||
player: ClassVar[int] = 1
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
LingoTestOptions.disable_forced_good_item = True
|
||||
super().world_setup(*args, **kwargs)
|
||||
|
||||
def remove_forced_good_item(self):
|
||||
location = self.multiworld.get_location("Second Room - Good Luck", self.player)
|
||||
self.remove(location.item)
|
||||
self.multiworld.itempool.append(location.item)
|
||||
self.multiworld.state.events.add(location)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
class LingoTestOptions:
|
||||
disable_forced_good_item: bool = False
|
||||
@@ -40,7 +40,7 @@ mentioned_panels = Set[]
|
||||
door_groups = {}
|
||||
|
||||
directives = Set["entrances", "panels", "doors", "paintings", "progression"]
|
||||
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"]
|
||||
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"]
|
||||
door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"]
|
||||
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@ pullpc
|
||||
|
||||
ScriptTX:
|
||||
STA $7FD4F1 ; (overwritten instruction)
|
||||
LDA $05AC ; load map number
|
||||
CMP.b #$F1 ; check if ancient cave final floor
|
||||
BNE +
|
||||
REP #$20
|
||||
LDA $7FD4EF ; read script item id
|
||||
CMP.w #$01C2 ; test for ancient key
|
||||
@@ -261,6 +264,9 @@ SpecialItemGet:
|
||||
BRA ++
|
||||
+: CMP.w #$01C2 ; ancient key
|
||||
BNE +
|
||||
LDA.w #$0008
|
||||
ORA $0796
|
||||
STA $0796 ; set ancient key EV flag ($C3)
|
||||
LDA.w #$0200
|
||||
ORA $0797
|
||||
STA $0797 ; set boss item EV flag ($D1)
|
||||
|
||||
Binary file not shown.
@@ -62,7 +62,7 @@ class MessengerWorld(World):
|
||||
"Money Wrench",
|
||||
], base_offset)}
|
||||
|
||||
required_client_version = (0, 4, 1)
|
||||
required_client_version = (0, 4, 2)
|
||||
|
||||
web = MessengerWeb()
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# The Messenger
|
||||
|
||||
## Quick Links
|
||||
- [Setup](../../../../tutorial/The%20Messenger/setup/en)
|
||||
- [Settings Page](../../../../games/The%20Messenger/player-settings)
|
||||
- [Setup](/tutorial/The%20Messenger/setup/en)
|
||||
- [Options Page](/games/The%20Messenger/player-options)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
|
||||
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
|
||||
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
|
||||
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
|
||||
|
||||
## What does randomization do in this game?
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# The Messenger Randomizer Setup Guide
|
||||
|
||||
## Quick Links
|
||||
- [Game Info](../../../../games/The%20Messenger/info/en)
|
||||
- [Settings Page](../../../../games/The%20Messenger/player-settings)
|
||||
- [Game Info](/games/The%20Messenger/info/en)
|
||||
- [Options Page](/games/The%20Messenger/player-options)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
|
||||
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
|
||||
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues
|
||||
1. Read the [Game Info Page](/games/The%20Messenger/info/en) for how the game works, caveats and known issues
|
||||
2. Download and install Courier Mod Loader using the instructions on the release page
|
||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||
3. Download and install the randomizer mod
|
||||
|
||||
@@ -63,7 +63,10 @@ class MessengerRules:
|
||||
"Searing Crags Seal - Triple Ball Spinner": self.has_vertical,
|
||||
"Searing Crags - Astral Tea Leaves":
|
||||
lambda state: state.can_reach("Ninja Village - Astral Seed", "Location", self.player),
|
||||
"Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player),
|
||||
"Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player)
|
||||
and (self.has_dart(state)
|
||||
or (self.has_wingsuit(state)
|
||||
and self.can_destroy_projectiles(state))),
|
||||
# glacial peak
|
||||
"Glacial Peak Seal - Ice Climbers": self.has_dart,
|
||||
"Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles,
|
||||
|
||||
@@ -1106,21 +1106,30 @@
|
||||
"parent_map": "MAP_ROUTE120",
|
||||
"locations": [
|
||||
"ITEM_ROUTE_120_NUGGET",
|
||||
"ITEM_ROUTE_120_FULL_HEAL",
|
||||
"ITEM_ROUTE_120_REVIVE",
|
||||
"ITEM_ROUTE_120_HYPER_POTION",
|
||||
"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
|
||||
"HIDDEN_ITEM_ROUTE_120_ZINC"
|
||||
],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_ROUTE120/NORTH",
|
||||
"REGION_ROUTE120/SOUTH_PONDS",
|
||||
"REGION_ROUTE121/WEST"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE120/SOUTH_PONDS": {
|
||||
"parent_map": "MAP_ROUTE120",
|
||||
"locations": [
|
||||
"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
|
||||
"ITEM_ROUTE_120_FULL_HEAL"
|
||||
],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": []
|
||||
},
|
||||
"REGION_ROUTE121/WEST": {
|
||||
"parent_map": "MAP_ROUTE121",
|
||||
"locations": [
|
||||
|
||||
@@ -626,6 +626,10 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"),
|
||||
can_surf
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"),
|
||||
can_surf
|
||||
)
|
||||
|
||||
# Route 121
|
||||
set_rule(
|
||||
|
||||
@@ -44,13 +44,17 @@ class TestScorchedSlabPond(PokemonEmeraldTestBase):
|
||||
|
||||
class TestSurf(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"npc_gifts": Toggle.option_true
|
||||
"npc_gifts": Toggle.option_true,
|
||||
"hidden_items": Toggle.option_true,
|
||||
"require_itemfinder": Toggle.option_false
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_surf(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
|
||||
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
|
||||
self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
|
||||
self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
|
||||
@@ -60,6 +64,8 @@ class TestSurf(PokemonEmeraldTestBase):
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
|
||||
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
|
||||
self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
|
||||
self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
|
||||
|
||||
@@ -414,7 +414,7 @@ class PokemonRedBlueWorld(World):
|
||||
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
|
||||
intervene_move = "Cut"
|
||||
elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player]
|
||||
and (((self.multiworld.accessibility[self.player] != "minimal" and
|
||||
and (((self.multiworld.accessibility[self.player] != "minimal" or
|
||||
(self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or
|
||||
self.multiworld.door_shuffle[self.player]))):
|
||||
intervene_move = "Flash"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@ from NetUtils import ClientStatus
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
from worlds._bizhawk import read, write, guarded_write
|
||||
|
||||
from worlds.pokemon_rb.locations import location_data
|
||||
from .locations import location_data
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
|
||||
@@ -1631,7 +1631,7 @@ def create_regions(self):
|
||||
connect(multiworld, player, "Cerulean City", "Route 24", one_way=True)
|
||||
connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player))
|
||||
connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True)
|
||||
connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", lambda state: logic.can_cut(state, player))
|
||||
connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True)
|
||||
connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player))
|
||||
connect(multiworld, player, "Cerulean City-Outskirts", "Route 5")
|
||||
connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True)
|
||||
@@ -1707,7 +1707,6 @@ def create_regions(self):
|
||||
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True)
|
||||
connect(multiworld, player, "Route 12-L", "Lavender Town")
|
||||
connect(multiworld, player, "Route 10-S", "Lavender Town")
|
||||
connect(multiworld, player, "Route 8-W", "Saffron City")
|
||||
connect(multiworld, player, "Route 8", "Lavender Town")
|
||||
connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player]))
|
||||
connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True)
|
||||
@@ -1831,7 +1830,8 @@ def create_regions(self):
|
||||
connect(multiworld, player, "Silph Co 6F", "Silph Co 6F-SW", lambda state: logic.card_key(state, 6, player))
|
||||
connect(multiworld, player, "Silph Co 7F", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player))
|
||||
connect(multiworld, player, "Silph Co 7F-SE", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player))
|
||||
connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player))
|
||||
connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F to Silph Co 8F-W (Card Key)")
|
||||
connect(multiworld, player, "Silph Co 8F-W", "Silph Co 8F", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F-W to Silph Co 8F (Card Key)")
|
||||
connect(multiworld, player, "Silph Co 9F", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player))
|
||||
connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player))
|
||||
connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player))
|
||||
@@ -1864,22 +1864,23 @@ def create_regions(self):
|
||||
# access to any part of a city will enable flying to the Pokemon Center
|
||||
connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)")
|
||||
connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True)
|
||||
connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)")
|
||||
connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)")
|
||||
connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)")
|
||||
connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)")
|
||||
connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)")
|
||||
connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)")
|
||||
connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)")
|
||||
connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)")
|
||||
connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)")
|
||||
|
||||
|
||||
# drops
|
||||
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)")
|
||||
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)")
|
||||
connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)")
|
||||
connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True)
|
||||
@@ -1888,7 +1889,7 @@ def create_regions(self):
|
||||
# If you haven't dropped the boulders, you'll go straight to B4F
|
||||
connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True)
|
||||
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)")
|
||||
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True)
|
||||
connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True)
|
||||
connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True)
|
||||
@@ -1944,7 +1945,8 @@ def create_regions(self):
|
||||
connect(multiworld, player, region.name, entrance_data["to"]["map"],
|
||||
lambda state: logic.rock_tunnel(state, player), one_way=True)
|
||||
else:
|
||||
connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True)
|
||||
connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True,
|
||||
name=entrance_data["name"] if "name" in entrance_data else None)
|
||||
|
||||
forced_connections = set()
|
||||
|
||||
|
||||
@@ -168,12 +168,12 @@ rom_addresses = {
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a61c,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a62a,
|
||||
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a638,
|
||||
"Event_SKC6F": 0x1a666,
|
||||
"Warps_SilphCo6F": 0x1a741,
|
||||
"Missable_Silph_Co_6F_Item_1": 0x1a791,
|
||||
"Missable_Silph_Co_6F_Item_2": 0x1a798,
|
||||
"Path_Pallet_Oak": 0x1a91e,
|
||||
"Path_Pallet_Player": 0x1a92b,
|
||||
"Event_SKC6F": 0x1a659,
|
||||
"Warps_SilphCo6F": 0x1a737,
|
||||
"Missable_Silph_Co_6F_Item_1": 0x1a787,
|
||||
"Missable_Silph_Co_6F_Item_2": 0x1a78e,
|
||||
"Path_Pallet_Oak": 0x1a914,
|
||||
"Path_Pallet_Player": 0x1a921,
|
||||
"Warps_CinnabarIsland": 0x1c026,
|
||||
"Warps_Route1": 0x1c0e9,
|
||||
"Option_Extra_Key_Items_B": 0x1ca46,
|
||||
|
||||
@@ -103,12 +103,10 @@ class RiskOfRainWorld(World):
|
||||
if self.options.dlc_sotv:
|
||||
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset)
|
||||
environments_pool = {**environments_pool, **environment_offset_table}
|
||||
environments_to_precollect = 5 if self.options.begin_with_loop else 1
|
||||
# percollect environments for each stage (or just stage 1)
|
||||
for i in range(environments_to_precollect):
|
||||
unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
|
||||
self.multiworld.push_precollected(self.create_item(unlock[0]))
|
||||
environments_pool.pop(unlock[0])
|
||||
# percollect starting environment for stage 1
|
||||
unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1)
|
||||
self.multiworld.push_precollected(self.create_item(unlock[0]))
|
||||
environments_pool.pop(unlock[0])
|
||||
|
||||
# Generate item pool
|
||||
itempool: List[str] = ["Beads of Fealty", "Radar Scanner"]
|
||||
|
||||
@@ -142,14 +142,6 @@ class FinalStageDeath(Toggle):
|
||||
display_name = "Final Stage Death is Win"
|
||||
|
||||
|
||||
class BeginWithLoop(Toggle):
|
||||
"""
|
||||
Enable to precollect a full loop of environments.
|
||||
Only has an effect with Explore Mode.
|
||||
"""
|
||||
display_name = "Begin With Loop"
|
||||
|
||||
|
||||
class DLC_SOTV(Toggle):
|
||||
"""
|
||||
Enable if you are using SOTV DLC.
|
||||
@@ -385,7 +377,6 @@ class ROR2Options(PerGameCommonOptions):
|
||||
total_revivals: TotalRevivals
|
||||
start_with_revive: StartWithRevive
|
||||
final_stage_death: FinalStageDeath
|
||||
begin_with_loop: BeginWithLoop
|
||||
dlc_sotv: DLC_SOTV
|
||||
death_link: DeathLink
|
||||
item_pickup_step: ItemPickupStep
|
||||
|
||||
@@ -638,7 +638,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
|
||||
add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player),
|
||||
lambda state: state.has(ItemName.shadow_air_shoes, player))
|
||||
add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
lambda state: state.has(ItemName.eggman_large_cannon, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player),
|
||||
lambda state: state.has(ItemName.tails_booster, player))
|
||||
|
||||
@@ -619,7 +619,7 @@ class SA2BWorld(World):
|
||||
for name in name_list_base:
|
||||
for char_idx in range(7):
|
||||
if char_idx < len(name):
|
||||
name_list_s.append(chao_name_conversion[name[char_idx]])
|
||||
name_list_s.append(chao_name_conversion.get(name[char_idx], 0x5F))
|
||||
else:
|
||||
name_list_s.append(0x00)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user