mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 07:35:37 -07:00
Merge branch 'main' into rework_accessibility
This commit is contained in:
@@ -491,7 +491,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 +504,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 +524,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):
|
||||
|
||||
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.")
|
||||
|
||||
11
Main.py
11
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")
|
||||
|
||||
@@ -57,6 +57,7 @@ Currently, the following games are supported:
|
||||
* Shivers
|
||||
* Heretic
|
||||
* Landstalker: The Treasures of King Nole
|
||||
* Final Fantasy Mystic Quest
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -90,6 +90,8 @@ def download_slot_file(room_id, player_id: int):
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
||||
elif slot_data.game == "Kingdom Hearts 2":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
|
||||
elif slot_data.game == "Final Fantasy Mystic Quest":
|
||||
fname = f"AP+{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmq"
|
||||
else:
|
||||
return "Game download not supported."
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
||||
|
||||
@@ -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)){
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download JSON File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
{% else %}
|
||||
No file to download for this game.
|
||||
{% endif %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
# Final Fantasy
|
||||
/worlds/ff1/ @jtoyoda
|
||||
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
# Heretic
|
||||
/worlds/heretic/ @Daivuk
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -137,7 +137,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))
|
||||
|
||||
@@ -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,7 @@ 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
|
||||
|
||||
import jinja2
|
||||
|
||||
@@ -63,7 +63,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)
|
||||
@@ -164,9 +164,7 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
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})
|
||||
|
||||
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 +175,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)
|
||||
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 +201,3 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
|
||||
# write the mod file
|
||||
mod.write()
|
||||
# clean up
|
||||
shutil.rmtree(mod_dir)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool
|
||||
from schema import Schema, Optional, And, Or
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
119
worlds/ffmq/Client.py
Normal file
119
worlds/ffmq/Client.py
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Regions import offset
|
||||
import logging
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
ROM_NAME = (0x7FC0, 0x7FD4 + 1 - 0x7FC0)
|
||||
|
||||
READ_DATA_START = 0xF50EA8
|
||||
READ_DATA_END = 0xF50FE7 + 1
|
||||
|
||||
GAME_FLAGS = (0xF50EA8, 64)
|
||||
COMPLETED_GAME = (0xF50F22, 1)
|
||||
BATTLEFIELD_DATA = (0xF50FD4, 20)
|
||||
|
||||
RECEIVED_DATA = (0xE01FF0, 3)
|
||||
|
||||
ITEM_CODE_START = 0x420000
|
||||
|
||||
IN_GAME_FLAG = (4 * 8) + 2
|
||||
|
||||
NPC_CHECKS = {
|
||||
4325676: ((6 * 8) + 4, False), # Old Man Level Forest
|
||||
4325677: ((3 * 8) + 6, True), # Kaeli Level Forest
|
||||
4325678: ((25 * 8) + 1, True), # Tristam
|
||||
4325680: ((26 * 8) + 0, True), # Aquaria Vendor Girl
|
||||
4325681: ((29 * 8) + 2, True), # Phoebe Wintry Cave
|
||||
4325682: ((25 * 8) + 6, False), # Mysterious Man (Life Temple)
|
||||
4325683: ((29 * 8) + 3, True), # Reuben Mine
|
||||
4325684: ((29 * 8) + 7, True), # Spencer
|
||||
4325685: ((29 * 8) + 6, False), # Venus Chest
|
||||
4325686: ((29 * 8) + 1, True), # Fireburg Tristam
|
||||
4325687: ((26 * 8) + 1, True), # Fireburg Vendor Girl
|
||||
4325688: ((14 * 8) + 4, True), # MegaGrenade Dude
|
||||
4325689: ((29 * 8) + 5, False), # Tristam's Chest
|
||||
4325690: ((29 * 8) + 4, True), # Arion
|
||||
4325691: ((29 * 8) + 0, True), # Windia Kaeli
|
||||
4325692: ((26 * 8) + 2, True), # Windia Vendor Girl
|
||||
|
||||
}
|
||||
|
||||
|
||||
def get_flag(data, flag):
|
||||
byte = int(flag / 8)
|
||||
bit = int(0x80 / (2 ** (flag % 8)))
|
||||
return (data[byte] & bit) > 0
|
||||
|
||||
|
||||
class FFMQClient(SNIClient):
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_read
|
||||
rom_name = await snes_read(ctx, *ROM_NAME)
|
||||
if rom_name is None:
|
||||
return False
|
||||
if rom_name[:2] != b"MQ":
|
||||
return False
|
||||
|
||||
ctx.rom = rom_name
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001
|
||||
return True
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
check_1 = await snes_read(ctx, 0xF53749, 1)
|
||||
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
|
||||
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
|
||||
check_2 = await snes_read(ctx, 0xF53749, 1)
|
||||
if check_1 == b'\x00' or check_2 == b'\x00':
|
||||
return
|
||||
|
||||
def get_range(data_range):
|
||||
return data[data_range[0] - READ_DATA_START:data_range[0] + data_range[1] - READ_DATA_START]
|
||||
completed_game = get_range(COMPLETED_GAME)
|
||||
battlefield_data = get_range(BATTLEFIELD_DATA)
|
||||
game_flags = get_range(GAME_FLAGS)
|
||||
|
||||
if game_flags is None:
|
||||
return
|
||||
if not get_flag(game_flags, IN_GAME_FLAG):
|
||||
return
|
||||
|
||||
if not ctx.finished_game:
|
||||
if completed_game[0] & 0x80 and game_flags[30] & 0x18:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
old_locations_checked = ctx.locations_checked.copy()
|
||||
|
||||
for container in range(256):
|
||||
if get_flag(game_flags, (0x20 * 8) + container):
|
||||
ctx.locations_checked.add(offset["Chest"] + container)
|
||||
|
||||
for location, data in NPC_CHECKS.items():
|
||||
if get_flag(game_flags, data[0]) is data[1]:
|
||||
ctx.locations_checked.add(location)
|
||||
|
||||
for battlefield in range(20):
|
||||
if battlefield_data[battlefield] == 0:
|
||||
ctx.locations_checked.add(offset["BattlefieldItem"] + battlefield + 1)
|
||||
|
||||
if old_locations_checked != ctx.locations_checked:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}])
|
||||
|
||||
if received[0] == 0:
|
||||
received_index = int.from_bytes(received[1:], "big")
|
||||
if received_index < len(ctx.items_received):
|
||||
item = ctx.items_received[received_index]
|
||||
received_index += 1
|
||||
code = (item.item - ITEM_CODE_START) + 1
|
||||
if code > 256:
|
||||
code -= 256
|
||||
snes_buffered_write(ctx, RECEIVED_DATA[0], bytes([code, *received_index.to_bytes(2, "big")]))
|
||||
await snes_flush_writes(ctx)
|
||||
298
worlds/ffmq/Items.py
Normal file
298
worlds/ffmq/Items.py
Normal file
@@ -0,0 +1,298 @@
|
||||
from BaseClasses import ItemClassification, Item
|
||||
|
||||
fillers = {"Cure Potion": 61, "Heal Potion": 52, "Refresher": 17, "Seed": 2, "Bomb Refill": 19,
|
||||
"Projectile Refill": 50}
|
||||
|
||||
|
||||
class ItemData:
|
||||
def __init__(self, item_id, classification, groups=(), data_name=None):
|
||||
self.groups = groups
|
||||
self.classification = classification
|
||||
self.id = None
|
||||
if item_id is not None:
|
||||
self.id = item_id + 0x420000
|
||||
self.data_name = data_name
|
||||
|
||||
|
||||
item_table = {
|
||||
"Elixir": ItemData(0, ItemClassification.progression, ["Key Items"]),
|
||||
"Tree Wither": ItemData(1, ItemClassification.progression, ["Key Items"]),
|
||||
"Wakewater": ItemData(2, ItemClassification.progression, ["Key Items"]),
|
||||
"Venus Key": ItemData(3, ItemClassification.progression, ["Key Items"]),
|
||||
"Multi Key": ItemData(4, ItemClassification.progression, ["Key Items"]),
|
||||
"Mask": ItemData(5, ItemClassification.progression, ["Key Items"]),
|
||||
"Magic Mirror": ItemData(6, ItemClassification.progression, ["Key Items"]),
|
||||
"Thunder Rock": ItemData(7, ItemClassification.progression, ["Key Items"]),
|
||||
"Captain's Cap": ItemData(8, ItemClassification.progression_skip_balancing, ["Key Items"]),
|
||||
"Libra Crest": ItemData(9, ItemClassification.progression, ["Key Items"]),
|
||||
"Gemini Crest": ItemData(10, ItemClassification.progression, ["Key Items"]),
|
||||
"Mobius Crest": ItemData(11, ItemClassification.progression, ["Key Items"]),
|
||||
"Sand Coin": ItemData(12, ItemClassification.progression, ["Key Items", "Coins"]),
|
||||
"River Coin": ItemData(13, ItemClassification.progression, ["Key Items", "Coins"]),
|
||||
"Sun Coin": ItemData(14, ItemClassification.progression, ["Key Items", "Coins"]),
|
||||
"Sky Coin": ItemData(15, ItemClassification.progression_skip_balancing, ["Key Items", "Coins"]),
|
||||
"Sky Fragment": ItemData(15 + 256, ItemClassification.progression_skip_balancing, ["Key Items"]),
|
||||
"Cure Potion": ItemData(16, ItemClassification.filler, ["Consumables"]),
|
||||
"Heal Potion": ItemData(17, ItemClassification.filler, ["Consumables"]),
|
||||
"Seed": ItemData(18, ItemClassification.filler, ["Consumables"]),
|
||||
"Refresher": ItemData(19, ItemClassification.filler, ["Consumables"]),
|
||||
"Exit Book": ItemData(20, ItemClassification.useful, ["Spells"]),
|
||||
"Cure Book": ItemData(21, ItemClassification.useful, ["Spells"]),
|
||||
"Heal Book": ItemData(22, ItemClassification.useful, ["Spells"]),
|
||||
"Life Book": ItemData(23, ItemClassification.useful, ["Spells"]),
|
||||
"Quake Book": ItemData(24, ItemClassification.useful, ["Spells"]),
|
||||
"Blizzard Book": ItemData(25, ItemClassification.useful, ["Spells"]),
|
||||
"Fire Book": ItemData(26, ItemClassification.useful, ["Spells"]),
|
||||
"Aero Book": ItemData(27, ItemClassification.useful, ["Spells"]),
|
||||
"Thunder Seal": ItemData(28, ItemClassification.useful, ["Spells"]),
|
||||
"White Seal": ItemData(29, ItemClassification.useful, ["Spells"]),
|
||||
"Meteor Seal": ItemData(30, ItemClassification.useful, ["Spells"]),
|
||||
"Flare Seal": ItemData(31, ItemClassification.useful, ["Spells"]),
|
||||
"Progressive Sword": ItemData(32 + 256, ItemClassification.progression, ["Weapons", "Swords"]),
|
||||
"Steel Sword": ItemData(32, ItemClassification.progression, ["Weapons", "Swords"]),
|
||||
"Knight Sword": ItemData(33, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]),
|
||||
"Excalibur": ItemData(34, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]),
|
||||
"Progressive Axe": ItemData(35 + 256, ItemClassification.progression, ["Weapons", "Axes"]),
|
||||
"Axe": ItemData(35, ItemClassification.progression, ["Weapons", "Axes"]),
|
||||
"Battle Axe": ItemData(36, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]),
|
||||
"Giant's Axe": ItemData(37, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]),
|
||||
"Progressive Claw": ItemData(38 + 256, ItemClassification.progression, ["Weapons", "Axes"]),
|
||||
"Cat Claw": ItemData(38, ItemClassification.progression, ["Weapons", "Claws"]),
|
||||
"Charm Claw": ItemData(39, ItemClassification.progression_skip_balancing, ["Weapons", "Claws"]),
|
||||
"Dragon Claw": ItemData(40, ItemClassification.progression, ["Weapons", "Claws"]),
|
||||
"Progressive Bomb": ItemData(41 + 256, ItemClassification.progression, ["Weapons", "Bombs"]),
|
||||
"Bomb": ItemData(41, ItemClassification.progression, ["Weapons", "Bombs"]),
|
||||
"Jumbo Bomb": ItemData(42, ItemClassification.progression_skip_balancing, ["Weapons", "Bombs"]),
|
||||
"Mega Grenade": ItemData(43, ItemClassification.progression, ["Weapons", "Bombs"]),
|
||||
# Ally-only equipment does nothing when received, no reason to put them in the datapackage
|
||||
#"Morning Star": ItemData(44, ItemClassification.progression, ["Weapons"]),
|
||||
#"Bow Of Grace": ItemData(45, ItemClassification.progression, ["Weapons"]),
|
||||
#"Ninja Star": ItemData(46, ItemClassification.progression, ["Weapons"]),
|
||||
|
||||
"Progressive Helm": ItemData(47 + 256, ItemClassification.useful, ["Helms"]),
|
||||
"Steel Helm": ItemData(47, ItemClassification.useful, ["Helms"]),
|
||||
"Moon Helm": ItemData(48, ItemClassification.useful, ["Helms"]),
|
||||
"Apollo Helm": ItemData(49, ItemClassification.useful, ["Helms"]),
|
||||
"Progressive Armor": ItemData(50 + 256, ItemClassification.useful, ["Armors"]),
|
||||
"Steel Armor": ItemData(50, ItemClassification.useful, ["Armors"]),
|
||||
"Noble Armor": ItemData(51, ItemClassification.useful, ["Armors"]),
|
||||
"Gaia's Armor": ItemData(52, ItemClassification.useful, ["Armors"]),
|
||||
#"Replica Armor": ItemData(53, ItemClassification.progression, ["Armors"]),
|
||||
#"Mystic Robes": ItemData(54, ItemClassification.progression, ["Armors"]),
|
||||
#"Flame Armor": ItemData(55, ItemClassification.progression, ["Armors"]),
|
||||
#"Black Robe": ItemData(56, ItemClassification.progression, ["Armors"]),
|
||||
"Progressive Shield": ItemData(57 + 256, ItemClassification.useful, ["Shields"]),
|
||||
"Steel Shield": ItemData(57, ItemClassification.useful, ["Shields"]),
|
||||
"Venus Shield": ItemData(58, ItemClassification.useful, ["Shields"]),
|
||||
"Aegis Shield": ItemData(59, ItemClassification.useful, ["Shields"]),
|
||||
#"Ether Shield": ItemData(60, ItemClassification.progression, ["Shields"]),
|
||||
"Progressive Accessory": ItemData(61 + 256, ItemClassification.useful, ["Accessories"]),
|
||||
"Charm": ItemData(61, ItemClassification.useful, ["Accessories"]),
|
||||
"Magic Ring": ItemData(62, ItemClassification.useful, ["Accessories"]),
|
||||
"Cupid Locket": ItemData(63, ItemClassification.useful, ["Accessories"]),
|
||||
|
||||
# these are understood by FFMQR and I could place these if I want, but it's easier to just let FFMQR
|
||||
# place them. I want an option to make shuffle battlefield rewards NOT color-code the battlefields,
|
||||
# and then I would make the non-item reward battlefields into AP checks and these would be put into those as
|
||||
# the item for AP. But there is no such option right now.
|
||||
# "54 XP": ItemData(96, ItemClassification.filler, data_name="Xp54"),
|
||||
# "99 XP": ItemData(97, ItemClassification.filler, data_name="Xp99"),
|
||||
# "540 XP": ItemData(98, ItemClassification.filler, data_name="Xp540"),
|
||||
# "744 XP": ItemData(99, ItemClassification.filler, data_name="Xp744"),
|
||||
# "816 XP": ItemData(100, ItemClassification.filler, data_name="Xp816"),
|
||||
# "1068 XP": ItemData(101, ItemClassification.filler, data_name="Xp1068"),
|
||||
# "1200 XP": ItemData(102, ItemClassification.filler, data_name="Xp1200"),
|
||||
# "2700 XP": ItemData(103, ItemClassification.filler, data_name="Xp2700"),
|
||||
# "2808 XP": ItemData(104, ItemClassification.filler, data_name="Xp2808"),
|
||||
# "150 Gp": ItemData(105, ItemClassification.filler, data_name="Gp150"),
|
||||
# "300 Gp": ItemData(106, ItemClassification.filler, data_name="Gp300"),
|
||||
# "600 Gp": ItemData(107, ItemClassification.filler, data_name="Gp600"),
|
||||
# "900 Gp": ItemData(108, ItemClassification.filler, data_name="Gp900"),
|
||||
# "1200 Gp": ItemData(109, ItemClassification.filler, data_name="Gp1200"),
|
||||
|
||||
|
||||
"Bomb Refill": ItemData(221, ItemClassification.filler, ["Refills"]),
|
||||
"Projectile Refill": ItemData(222, ItemClassification.filler, ["Refills"]),
|
||||
#"None": ItemData(255, ItemClassification.progression, []),
|
||||
|
||||
"Kaeli 1": ItemData(None, ItemClassification.progression),
|
||||
"Kaeli 2": ItemData(None, ItemClassification.progression),
|
||||
"Tristam": ItemData(None, ItemClassification.progression),
|
||||
"Phoebe 1": ItemData(None, ItemClassification.progression),
|
||||
"Reuben 1": ItemData(None, ItemClassification.progression),
|
||||
"Reuben Dad Saved": ItemData(None, ItemClassification.progression),
|
||||
"Otto": ItemData(None, ItemClassification.progression),
|
||||
"Captain Mac": ItemData(None, ItemClassification.progression),
|
||||
"Ship Steering Wheel": ItemData(None, ItemClassification.progression),
|
||||
"Minotaur": ItemData(None, ItemClassification.progression),
|
||||
"Flamerus Rex": ItemData(None, ItemClassification.progression),
|
||||
"Phanquid": ItemData(None, ItemClassification.progression),
|
||||
"Freezer Crab": ItemData(None, ItemClassification.progression),
|
||||
"Ice Golem": ItemData(None, ItemClassification.progression),
|
||||
"Jinn": ItemData(None, ItemClassification.progression),
|
||||
"Medusa": ItemData(None, ItemClassification.progression),
|
||||
"Dualhead Hydra": ItemData(None, ItemClassification.progression),
|
||||
"Gidrah": ItemData(None, ItemClassification.progression),
|
||||
"Dullahan": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu": ItemData(None, ItemClassification.progression),
|
||||
"Aquaria Plaza": ItemData(None, ItemClassification.progression),
|
||||
"Summer Aquaria": ItemData(None, ItemClassification.progression),
|
||||
"Reuben Mine": ItemData(None, ItemClassification.progression),
|
||||
"Alive Forest": ItemData(None, ItemClassification.progression),
|
||||
"Rainbow Bridge": ItemData(None, ItemClassification.progression),
|
||||
"Collapse Spencer's Cave": ItemData(None, ItemClassification.progression),
|
||||
"Ship Liberated": ItemData(None, ItemClassification.progression),
|
||||
"Ship Loaned": ItemData(None, ItemClassification.progression),
|
||||
"Ship Dock Access": ItemData(None, ItemClassification.progression),
|
||||
"Stone Golem": ItemData(None, ItemClassification.progression),
|
||||
"Twinhead Wyvern": ItemData(None, ItemClassification.progression),
|
||||
"Zuh": ItemData(None, ItemClassification.progression),
|
||||
|
||||
"Libra Temple Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Life Temple Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Aquaria Vendor Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Fireburg Vendor Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Fireburg Grenademan Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Sealed Temple Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Wintry Temple Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Kaidge Temple Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Light Temple Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Windia Kids Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Windia Dock Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Ship Dock Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Alive Forest Libra Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Alive Forest Gemini Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Alive Forest Mobius Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Wood House Libra Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Wood House Gemini Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Wood House Mobius Crest Tile": ItemData(None, ItemClassification.progression),
|
||||
"Barrel Pushed": ItemData(None, ItemClassification.progression),
|
||||
"Long Spine Bombed": ItemData(None, ItemClassification.progression),
|
||||
"Short Spine Bombed": ItemData(None, ItemClassification.progression),
|
||||
"Skull 1 Bombed": ItemData(None, ItemClassification.progression),
|
||||
"Skull 2 Bombed": ItemData(None, ItemClassification.progression),
|
||||
"Ice Pyramid 1F Statue": ItemData(None, ItemClassification.progression),
|
||||
"Ice Pyramid 3F Statue": ItemData(None, ItemClassification.progression),
|
||||
"Ice Pyramid 4F Statue": ItemData(None, ItemClassification.progression),
|
||||
"Ice Pyramid 5F Statue": ItemData(None, ItemClassification.progression),
|
||||
"Spencer Cave Libra Block Bombed": ItemData(None, ItemClassification.progression),
|
||||
"Lava Dome Plate": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 2F Lock": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 4F Lock": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 6F Lock": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 1F": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 2F": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 3F": ItemData(None, ItemClassification.progression),
|
||||
"Pazuzu 4F": ItemData(None, ItemClassification.progression),
|
||||
"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),
|
||||
|
||||
}
|
||||
|
||||
prog_map = {
|
||||
"Swords": "Progressive Sword",
|
||||
"Axes": "Progressive Axe",
|
||||
"Claws": "Progressive Claw",
|
||||
"Bombs": "Progressive Bomb",
|
||||
"Shields": "Progressive Shield",
|
||||
"Armors": "Progressive Armor",
|
||||
"Helms": "Progressive Helm",
|
||||
"Accessories": "Progressive Accessory",
|
||||
}
|
||||
|
||||
|
||||
def yaml_item(text):
|
||||
if text == "CaptainCap":
|
||||
return "Captain's Cap"
|
||||
elif text == "WakeWater":
|
||||
return "Wakewater"
|
||||
return "".join(
|
||||
[(" " + c if (c.isupper() or c.isnumeric()) and not (text[i - 1].isnumeric() and c == "F") else c) for
|
||||
i, c in enumerate(text)]).strip()
|
||||
|
||||
|
||||
item_groups = {}
|
||||
for item, data in item_table.items():
|
||||
for group in data.groups:
|
||||
item_groups[group] = item_groups.get(group, []) + [item]
|
||||
|
||||
|
||||
def create_items(self) -> None:
|
||||
items = []
|
||||
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
|
||||
if self.multiworld.progressive_gear[self.player]:
|
||||
for item_group in prog_map:
|
||||
if starting_weapon in self.item_name_groups[item_group]:
|
||||
starting_weapon = prog_map[item_group]
|
||||
break
|
||||
self.multiworld.push_precollected(self.create_item(starting_weapon))
|
||||
self.multiworld.push_precollected(self.create_item("Steel Armor"))
|
||||
if self.multiworld.sky_coin_mode[self.player] == "start_with":
|
||||
self.multiworld.push_precollected(self.create_item("Sky Coin"))
|
||||
|
||||
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
|
||||
|
||||
def add_item(item_name):
|
||||
if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name:
|
||||
return
|
||||
if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key:
|
||||
return
|
||||
if self.multiworld.progressive_gear[self.player]:
|
||||
for item_group in prog_map:
|
||||
if item_name in self.item_name_groups[item_group]:
|
||||
item_name = prog_map[item_group]
|
||||
break
|
||||
if item_name == "Sky Coin":
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
for _ in range(40):
|
||||
items.append(self.create_item("Sky Fragment"))
|
||||
return
|
||||
elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals":
|
||||
items.append(self.create_filler())
|
||||
return
|
||||
if item_name in precollected_item_names:
|
||||
items.append(self.create_filler())
|
||||
return
|
||||
i = self.create_item(item_name)
|
||||
if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"):
|
||||
i.classification = ItemClassification.useful
|
||||
if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and
|
||||
item_name == "Exit Book"):
|
||||
i.classification = ItemClassification.progression
|
||||
items.append(i)
|
||||
|
||||
for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"):
|
||||
for item in self.item_name_groups[item_group]:
|
||||
add_item(item)
|
||||
|
||||
if self.multiworld.brown_boxes[self.player] == "include":
|
||||
filler_items = []
|
||||
for item, count in fillers.items():
|
||||
filler_items += [self.create_item(item) for _ in range(count)]
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
self.multiworld.random.shuffle(filler_items)
|
||||
filler_items = filler_items[39:]
|
||||
items += filler_items
|
||||
|
||||
self.multiworld.itempool += items
|
||||
|
||||
if len(self.multiworld.player_ids) > 1:
|
||||
early_choices = ["Sand Coin", "River Coin"]
|
||||
early_item = self.multiworld.random.choice(early_choices)
|
||||
self.multiworld.early_items[self.player][early_item] = 1
|
||||
|
||||
|
||||
class FFMQItem(Item):
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
type = None
|
||||
|
||||
def __init__(self, name, player: int = None):
|
||||
item_data = item_table[name]
|
||||
super(FFMQItem, self).__init__(
|
||||
name,
|
||||
item_data.classification,
|
||||
item_data.id, player
|
||||
)
|
||||
22
worlds/ffmq/LICENSE
Normal file
22
worlds/ffmq/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Alex "Alchav" Avery
|
||||
Copyright (c) 2023 wildham
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
356
worlds/ffmq/Options.py
Normal file
356
worlds/ffmq/Options.py
Normal file
@@ -0,0 +1,356 @@
|
||||
from Options import Choice, FreeText, Toggle, Range
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
"""Placement logic sets the rules that will be applied when placing items. Friendly: Required Items to clear a
|
||||
dungeon will never be placed in that dungeon to avoid the need to revisit it. Also, the Magic Mirror and the Mask
|
||||
will always be available before Ice Pyramid and Volcano, respectively. Note: If Dungeons are shuffled, Friendly
|
||||
logic will only ensure the availability of the Mirror and the Mask. Standard: Items are randomly placed and logic
|
||||
merely verifies that they're all accessible. As for Region access, only the Coins are considered. Expert: Same as
|
||||
Standard, but Items Placement logic also includes other routes than Coins: the Crests Teleporters, the
|
||||
Fireburg-Aquaria Lava bridge and the Sealed Temple Exit trick."""
|
||||
option_friendly = 0
|
||||
option_standard = 1
|
||||
option_expert = 2
|
||||
default = 1
|
||||
display_name = "Logic"
|
||||
|
||||
|
||||
class BrownBoxes(Choice):
|
||||
"""Include the 201 brown box locations from the original game. Brown Boxes are all the boxes that contained a
|
||||
consumable in the original game. If shuffle is chosen, the consumables contained will be shuffled but the brown
|
||||
boxes will not be Archipelago location checks."""
|
||||
option_exclude = 0
|
||||
option_include = 1
|
||||
option_shuffle = 2
|
||||
default = 1
|
||||
display_name = "Brown Boxes"
|
||||
|
||||
|
||||
class SkyCoinMode(Choice):
|
||||
"""Configure how the Sky Coin is acquired. With standard, the Sky Coin will be placed randomly. With Start With, the
|
||||
Sky Coin will be in your inventory at the start of the game. With Save The Crystals, the Sky Coin will be acquired
|
||||
once you save all 4 crystals. With Shattered Sky Coin, the Sky Coin is split in 40 fragments; you can enter Doom
|
||||
Castle once the required amount is found. Shattered Sky Coin will force brown box locations to be included."""
|
||||
option_standard = 0
|
||||
option_start_with = 1
|
||||
option_save_the_crystals = 2
|
||||
option_shattered_sky_coin = 3
|
||||
default = 0
|
||||
display_name = "Sky Coin Mode"
|
||||
|
||||
|
||||
class ShatteredSkyCoinQuantity(Choice):
|
||||
"""Configure the number of the 40 Sky Coin Fragments required to enter the Doom Castle. Only has an effect if
|
||||
Sky Coin Mode is set to shattered. Low: 16. Mid: 24. High: 32. Random Narrow: random between 16 and 32.
|
||||
Random Wide: random between 10 and 38."""
|
||||
option_low_16 = 0
|
||||
option_mid_24 = 1
|
||||
option_high_32 = 2
|
||||
option_random_narrow = 3
|
||||
option_random_wide = 4
|
||||
default = 1
|
||||
display_name = "Shattered Sky Coin"
|
||||
|
||||
|
||||
class StartingWeapon(Choice):
|
||||
"""Choose your starting weapon."""
|
||||
display_name = "Starting Weapon"
|
||||
option_steel_sword = 0
|
||||
option_axe = 1
|
||||
option_cat_claw = 2
|
||||
option_bomb = 3
|
||||
default = "random"
|
||||
|
||||
|
||||
class ProgressiveGear(Toggle):
|
||||
"""Pieces of gear are always acquired from weakest to strongest in a set."""
|
||||
display_name = "Progressive Gear"
|
||||
|
||||
|
||||
class EnemiesDensity(Choice):
|
||||
"""Set how many of the original enemies are on each map."""
|
||||
display_name = "Enemies Density"
|
||||
option_all = 0
|
||||
option_three_quarter = 1
|
||||
option_half = 2
|
||||
option_quarter = 3
|
||||
option_none = 4
|
||||
|
||||
|
||||
class EnemyScaling(Choice):
|
||||
"""Superclass for enemy scaling options."""
|
||||
option_quarter = 0
|
||||
option_half = 1
|
||||
option_three_quarter = 2
|
||||
option_normal = 3
|
||||
option_one_and_quarter = 4
|
||||
option_one_and_half = 5
|
||||
option_double = 6
|
||||
option_double_and_half = 7
|
||||
option_triple = 8
|
||||
|
||||
|
||||
class EnemiesScalingLower(EnemyScaling):
|
||||
"""Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones."""
|
||||
display_name = "Enemies Scaling Lower"
|
||||
default = 0
|
||||
|
||||
|
||||
class EnemiesScalingUpper(EnemyScaling):
|
||||
"""Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones."""
|
||||
display_name = "Enemies Scaling Upper"
|
||||
default = 4
|
||||
|
||||
|
||||
class BossesScalingLower(EnemyScaling):
|
||||
"""Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and
|
||||
the Dark King."""
|
||||
display_name = "Bosses Scaling Lower"
|
||||
default = 0
|
||||
|
||||
|
||||
class BossesScalingUpper(EnemyScaling):
|
||||
"""Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and
|
||||
the Dark King."""
|
||||
display_name = "Bosses Scaling Upper"
|
||||
default = 4
|
||||
|
||||
|
||||
class EnemizerAttacks(Choice):
|
||||
"""Shuffles enemy attacks. Standard: No shuffle. Safe: Randomize every attack but leave out self-destruct and Dark
|
||||
King attacks. Chaos: Randomize and include self-destruct and Dark King attacks. Self Destruct: Every enemy
|
||||
self-destructs. Simple Shuffle: Instead of randomizing, shuffle one monster's attacks to another. Dark King is left
|
||||
vanilla."""
|
||||
display_name = "Enemizer Attacks"
|
||||
option_normal = 0
|
||||
option_safe = 1
|
||||
option_chaos = 2
|
||||
option_self_destruct = 3
|
||||
option_simple_shuffle = 4
|
||||
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"
|
||||
default = 1
|
||||
|
||||
|
||||
class ProgressiveFormations(Choice):
|
||||
"""Enemies' formations are selected by regions, with the weakest formations always selected in Foresta and the
|
||||
strongest in Windia. Disabled: Standard formations are used. Regions Strict: Formations will come exclusively
|
||||
from the current region, whatever the map is. Regions Keep Type: Formations will keep the original formation type
|
||||
and match with the nearest power level."""
|
||||
display_name = "Progressive Formations"
|
||||
option_disabled = 0
|
||||
option_regions_strict = 1
|
||||
option_regions_keep_type = 2
|
||||
|
||||
|
||||
class DoomCastle(Choice):
|
||||
"""Configure how you reach the Dark King. With Standard, you need to defeat all four bosses and their floors to
|
||||
reach the Dark King. With Boss Rush, only the bosses are blocking your way in the corridor to the Dark King's room.
|
||||
With Dark King Only, the way to the Dark King is free of any obstacle."""
|
||||
display_name = "Doom Castle"
|
||||
option_standard = 0
|
||||
option_boss_rush = 1
|
||||
option_dark_king_only = 2
|
||||
|
||||
|
||||
class DoomCastleShortcut(Toggle):
|
||||
"""Create a shortcut granting access from the start to Doom Castle at Focus Tower's entrance.
|
||||
Also modify the Desert floor, so it can be navigated without the Mega Grenades and the Dragon Claw."""
|
||||
display_name = "Doom Castle Shortcut"
|
||||
|
||||
|
||||
class TweakFrustratingDungeons(Toggle):
|
||||
"""Make some small changes to a few of the most annoying dungeons. Ice Pyramid: Add 3 shortcuts on the 1st floor.
|
||||
Giant Tree: Add shortcuts on the 1st and 4th floors and curtail mushrooms population.
|
||||
Pazuzu's Tower: Staircases are devoid of enemies (regardless of Enemies Density settings)."""
|
||||
display_name = "Tweak Frustrating Dungeons"
|
||||
|
||||
|
||||
class MapShuffle(Choice):
|
||||
"""None: No shuffle. Overworld: Only shuffle the Overworld locations. Dungeons: Only shuffle the dungeons' floors
|
||||
amongst themselves. Temples and Towns aren't included. Overworld And Dungeons: Shuffle the Overworld and dungeons
|
||||
at the same time. Everything: Shuffle the Overworld, dungeons, temples and towns all amongst each others.
|
||||
When dungeons are shuffled, defeating Pazuzu won't teleport you to the 7th floor, you have to get there normally to
|
||||
save the Crystal and get Pazuzu's Chest."""
|
||||
display_name = "Map Shuffle"
|
||||
option_none = 0
|
||||
option_overworld = 1
|
||||
option_dungeons = 2
|
||||
option_overworld_and_dungeons = 3
|
||||
option_everything = 4
|
||||
default = 0
|
||||
|
||||
|
||||
class CrestShuffle(Toggle):
|
||||
"""Shuffle the Crest tiles amongst themselves."""
|
||||
display_name = "Crest Shuffle"
|
||||
|
||||
|
||||
class MapShuffleSeed(FreeText):
|
||||
"""If this is a number, it will be used as a set seed number for Map, Crest, and Battlefield Reward shuffles.
|
||||
If this is "random" the seed will be chosen randomly. If it is any other text, it will be used as a seed group name.
|
||||
All players using the same seed group name will get the same shuffle results, as long as their Map Shuffle,
|
||||
Crest Shuffle, and Shuffle Battlefield Rewards settings are the same."""
|
||||
display_name = "Map Shuffle Seed"
|
||||
default = "random"
|
||||
|
||||
|
||||
class LevelingCurve(Choice):
|
||||
"""Adjust the level gain rate."""
|
||||
display_name = "Leveling Curve"
|
||||
option_half = 0
|
||||
option_normal = 1
|
||||
option_one_and_half = 2
|
||||
option_double = 3
|
||||
option_double_and_half = 4
|
||||
option_triple = 5
|
||||
option_quadruple = 6
|
||||
default = 4
|
||||
|
||||
|
||||
class ShuffleBattlefieldRewards(Toggle):
|
||||
"""Shuffle the type of reward (Item, XP, GP) given by battlefields and color code them by reward type.
|
||||
Blue: Give an item. Grey: Give XP. Green: Give GP."""
|
||||
display_name = "Shuffle Battlefield Rewards"
|
||||
|
||||
|
||||
class BattlefieldsBattlesQuantities(Choice):
|
||||
"""Adjust the number of battles that need to be fought to get a battlefield's reward."""
|
||||
display_name = "Battlefields Battles Quantity"
|
||||
option_ten = 0
|
||||
option_seven = 1
|
||||
option_five = 2
|
||||
option_three = 3
|
||||
option_one = 4
|
||||
option_random_one_through_five = 5
|
||||
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.
|
||||
Standard 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_standard_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,
|
||||
"sky_coin_mode": SkyCoinMode,
|
||||
"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,
|
||||
"doom_castle_shortcut": DoomCastleShortcut,
|
||||
"tweak_frustrating_dungeons": TweakFrustratingDungeons,
|
||||
"map_shuffle": MapShuffle,
|
||||
"crest_shuffle": CrestShuffle,
|
||||
"shuffle_battlefield_rewards": ShuffleBattlefieldRewards,
|
||||
"map_shuffle_seed": MapShuffleSeed,
|
||||
"battlefields_battles_quantities": BattlefieldsBattlesQuantities,
|
||||
}
|
||||
125
worlds/ffmq/Output.py
Normal file
125
worlds/ffmq/Output.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import yaml
|
||||
import os
|
||||
import zipfile
|
||||
from copy import deepcopy
|
||||
from .Regions import object_id_table
|
||||
from Main import __version__
|
||||
from worlds.Files import APContainer
|
||||
import pkgutil
|
||||
|
||||
settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader)
|
||||
|
||||
|
||||
def generate_output(self, output_directory):
|
||||
def output_item_name(item):
|
||||
if item.player == self.player:
|
||||
if item.code > 0x420000 + 256:
|
||||
item_name = self.item_id_to_name[item.code - 256]
|
||||
else:
|
||||
item_name = item.name
|
||||
item_name = "".join(item_name.split("'"))
|
||||
item_name = "".join(item_name.split(" "))
|
||||
else:
|
||||
if item.advancement or item.useful or (item.trap and
|
||||
self.multiworld.per_slot_randoms[self.player].randint(0, 1)):
|
||||
item_name = "APItem"
|
||||
else:
|
||||
item_name = "APItemFiller"
|
||||
return item_name
|
||||
|
||||
item_placement = []
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if location.type != "Trigger":
|
||||
item_placement.append({"object_id": object_id_table[location.name], "type": location.type, "content":
|
||||
output_item_name(location.item), "player": self.multiworld.player_name[location.item.player],
|
||||
"item_name": location.item.name})
|
||||
|
||||
def cc(option):
|
||||
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]),
|
||||
"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
|
||||
|
||||
rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21]
|
||||
self.rom_name = bytearray(rom_name,
|
||||
'utf8')
|
||||
self.rom_name_available_event.set()
|
||||
|
||||
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]]
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
starting_items.append("SkyCoin")
|
||||
|
||||
file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq")
|
||||
|
||||
APMQ = APMQFile(file_path, player=self.player, player_name=self.multiworld.player_name[self.player])
|
||||
with zipfile.ZipFile(file_path, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
zf.writestr("itemplacement.yaml", yaml.dump(item_placement))
|
||||
zf.writestr("flagset.yaml", yaml.dump(options))
|
||||
zf.writestr("startingitems.yaml", yaml.dump(starting_items))
|
||||
zf.writestr("setup.yaml", yaml.dump(setup))
|
||||
zf.writestr("rooms.yaml", yaml.dump(self.rooms))
|
||||
|
||||
APMQ.write_contents(zf)
|
||||
|
||||
|
||||
class APMQFile(APContainer):
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
|
||||
def get_manifest(self):
|
||||
manifest = super().get_manifest()
|
||||
manifest["patch_file_ending"] = ".apmq"
|
||||
return manifest
|
||||
251
worlds/ffmq/Regions.py
Normal file
251
worlds/ffmq/Regions.py
Normal file
@@ -0,0 +1,251 @@
|
||||
from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification
|
||||
from worlds.generic.Rules import add_rule
|
||||
from .Items import item_groups, yaml_item
|
||||
import pkgutil
|
||||
import yaml
|
||||
|
||||
rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader)
|
||||
entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)}
|
||||
|
||||
object_id_table = {}
|
||||
object_type_table = {}
|
||||
offset = {"Chest": 0x420000, "Box": 0x420000, "NPC": 0x420000 + 300, "BattlefieldItem": 0x420000 + 350}
|
||||
for room in rooms:
|
||||
for object in room["game_objects"]:
|
||||
if "Hero Chest" in object["name"] or object["type"] == "Trigger":
|
||||
continue
|
||||
if object["type"] in ("BattlefieldItem", "BattlefieldXp", "BattlefieldGp"):
|
||||
object_type_table[object["name"]] = "BattlefieldItem"
|
||||
elif object["type"] in ("Chest", "NPC", "Box"):
|
||||
object_type_table[object["name"]] = object["type"]
|
||||
object_id_table[object["name"]] = object["object_id"]
|
||||
|
||||
location_table = {loc_name: offset[object_type_table[loc_name]] + obj_id for loc_name, obj_id in
|
||||
object_id_table.items()}
|
||||
|
||||
weapons = ("Claw", "Bomb", "Sword", "Axe")
|
||||
crest_warps = [51, 52, 53, 76, 96, 108, 158, 171, 175, 191, 275, 276, 277, 308, 334, 336, 396, 397]
|
||||
|
||||
|
||||
def process_rules(spot, access):
|
||||
for weapon in weapons:
|
||||
if weapon in access:
|
||||
add_rule(spot, lambda state, w=weapon: state.has_any(item_groups[w + "s"], spot.player))
|
||||
access = [yaml_item(rule) for rule in access if rule not in weapons]
|
||||
add_rule(spot, lambda state: state.has_all(access, spot.player))
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, room_id=None, locations=None, links=None):
|
||||
if links is None:
|
||||
links = []
|
||||
ret = Region(name, player, world)
|
||||
if locations:
|
||||
for location in locations:
|
||||
location.parent_region = ret
|
||||
ret.locations.append(location)
|
||||
ret.links = links
|
||||
ret.id = room_id
|
||||
return ret
|
||||
|
||||
|
||||
def get_entrance_to(entrance_to):
|
||||
for room in rooms:
|
||||
if room["id"] == entrance_to["target_room"]:
|
||||
for link in room["links"]:
|
||||
if link["target_room"] == entrance_to["room"]:
|
||||
return link
|
||||
else:
|
||||
raise Exception(f"Did not find entrance {entrance_to}")
|
||||
|
||||
|
||||
def create_regions(self):
|
||||
|
||||
menu_region = create_region(self.multiworld, self.player, "Menu")
|
||||
self.multiworld.regions.append(menu_region)
|
||||
|
||||
for room in self.rooms:
|
||||
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") 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", [])
|
||||
dark_king.parent_region = dark_king_room
|
||||
dark_king.place_locked_item(self.create_item("Dark King"))
|
||||
dark_king_room.locations.append(dark_king)
|
||||
|
||||
connection = Entrance(self.player, f"Enter Overworld", menu_region)
|
||||
connection.connect(self.multiworld.get_region("Overworld", self.player))
|
||||
menu_region.exits.append(connection)
|
||||
|
||||
for region in self.multiworld.get_regions(self.player):
|
||||
for link in region.links:
|
||||
for connect_room in self.multiworld.get_regions(self.player):
|
||||
if connect_room.id == link["target_room"]:
|
||||
connection = Entrance(self.player, entrance_names[link["entrance"]] if "entrance" in link and
|
||||
link["entrance"] != -1 else f"{region.name} to {connect_room.name}", region)
|
||||
if "entrance" in link and link["entrance"] != -1:
|
||||
spoiler = False
|
||||
if link["entrance"] in crest_warps:
|
||||
if self.multiworld.crest_shuffle[self.player]:
|
||||
spoiler = True
|
||||
elif self.multiworld.map_shuffle[self.player] == "everything":
|
||||
spoiler = True
|
||||
elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons",
|
||||
"none"):
|
||||
spoiler = True
|
||||
elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none",
|
||||
"overworld"):
|
||||
spoiler = True
|
||||
|
||||
if spoiler:
|
||||
self.multiworld.spoiler.set_entrance(entrance_names[link["entrance"]], connect_room.name,
|
||||
'both', self.player)
|
||||
if link["access"]:
|
||||
process_rules(connection, link["access"])
|
||||
region.exits.append(connection)
|
||||
connection.connect(connect_room)
|
||||
break
|
||||
|
||||
non_dead_end_crest_rooms = [
|
||||
'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room',
|
||||
'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge',
|
||||
'Windia Kid House Basement', 'Windia Old People House Basement'
|
||||
]
|
||||
|
||||
non_dead_end_crest_warps = [
|
||||
'Libra Temple - Libra Tile Script', 'Aquaria Gemini Room - Gemini Script',
|
||||
'GrenadeMan Mobius Room - Mobius Teleporter Script', 'Fireburg Gemini Room - Gemini Teleporter Script',
|
||||
'Sealed Temple - Gemini Tile Script', 'Alive Forest - Libra Teleporter Script',
|
||||
'Alive Forest - Gemini Teleporter Script', 'Alive Forest - Mobius Teleporter Script',
|
||||
'Kaidge Temple - Mobius Teleporter Script', 'Windia Kid House Basement - Mobius Teleporter',
|
||||
'Windia Old People House Basement - Mobius Teleporter Script',
|
||||
]
|
||||
|
||||
|
||||
vendor_locations = ["Aquaria - Vendor", "Fireburg - Vendor", "Windia - Vendor"]
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Dark King", self.player)
|
||||
|
||||
def hard_boss_logic(state):
|
||||
return state.has_all(["River Coin", "Sand Coin"], self.player)
|
||||
|
||||
add_rule(self.multiworld.get_location("Pazuzu 1F", self.player), hard_boss_logic)
|
||||
add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic)
|
||||
add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic)
|
||||
|
||||
if self.multiworld.map_shuffle[self.player]:
|
||||
for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"):
|
||||
loc = self.multiworld.get_location(boss, self.player)
|
||||
checked_regions = {loc.parent_region}
|
||||
|
||||
def check_foresta(region):
|
||||
if region.name == "Subregion Foresta":
|
||||
add_rule(loc, hard_boss_logic)
|
||||
return True
|
||||
elif "Subregion" in region.name:
|
||||
return True
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region not in checked_regions:
|
||||
checked_regions.add(entrance.parent_region)
|
||||
if check_foresta(entrance.parent_region):
|
||||
return True
|
||||
check_foresta(loc.parent_region)
|
||||
|
||||
if self.multiworld.logic[self.player] == "friendly":
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||
["MagicMirror"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player),
|
||||
["Mask"])
|
||||
if self.multiworld.map_shuffle[self.player] in ("none", "overworld"):
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player),
|
||||
["Bomb"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player),
|
||||
["Bomb", "Claw"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||
["Bomb", "Claw"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Mine", self.player),
|
||||
["MegaGrenade", "Claw", "Reuben1"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Lava Dome", self.player),
|
||||
["MegaGrenade"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Giant Tree", self.player),
|
||||
["DragonClaw", "Axe"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Mount Gale", self.player),
|
||||
["DragonClaw"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Pazuzu Tower", self.player),
|
||||
["DragonClaw", "Bomb"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Mac Ship", self.player),
|
||||
["DragonClaw", "CaptainCap"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player),
|
||||
["DragonClaw", "CaptainCap"])
|
||||
|
||||
if self.multiworld.logic[self.player] == "expert":
|
||||
if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]:
|
||||
inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player)
|
||||
connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room)
|
||||
connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player))
|
||||
connection.access_rule = lambda state: state.has("Exit Book", self.player)
|
||||
inner_room.exits.append(connection)
|
||||
else:
|
||||
for crest_warp in non_dead_end_crest_warps:
|
||||
entrance = self.multiworld.get_entrance(crest_warp, self.player)
|
||||
if entrance.connected_region.name in non_dead_end_crest_rooms:
|
||||
entrance.access_rule = lambda state: False
|
||||
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value]
|
||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||
lambda state: state.has("Sky Fragment", self.player, logic_coins)
|
||||
elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals":
|
||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||
lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player)
|
||||
elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"):
|
||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||
lambda state: state.has("Sky Coin", self.player)
|
||||
|
||||
|
||||
def stage_set_rules(multiworld):
|
||||
# If there's no enemies, there's no repeatable income sources
|
||||
no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest")
|
||||
if multiworld.enemies_density[player] == "none"]
|
||||
if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler,
|
||||
ItemClassification.trap)]) > len([player for player in no_enemies_players if
|
||||
multiworld.accessibility[player] == "minimal"]) * 3):
|
||||
for player in no_enemies_players:
|
||||
for location in vendor_locations:
|
||||
if multiworld.accessibility[player] == "locations":
|
||||
print("exclude")
|
||||
multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
print("unreachable")
|
||||
multiworld.get_location(location, player).access_rule = lambda state: False
|
||||
else:
|
||||
# There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing
|
||||
# advancement items so that useful items can be placed.
|
||||
print("no advancement")
|
||||
for player in no_enemies_players:
|
||||
for location in vendor_locations:
|
||||
multiworld.get_location(location, player).item_rule = lambda item: not item.advancement
|
||||
|
||||
|
||||
|
||||
|
||||
class FFMQLocation(Location):
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
|
||||
def __init__(self, player, name, address, loc_type, access=None, event=None):
|
||||
super(FFMQLocation, self).__init__(
|
||||
player, name,
|
||||
address
|
||||
)
|
||||
self.type = loc_type
|
||||
if access:
|
||||
process_rules(self, access)
|
||||
if event:
|
||||
self.place_locked_item(event)
|
||||
219
worlds/ffmq/__init__.py
Normal file
219
worlds/ffmq/__init__.py
Normal file
@@ -0,0 +1,219 @@
|
||||
import Utils
|
||||
import settings
|
||||
import base64
|
||||
import threading
|
||||
import requests
|
||||
import yaml
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Tutorial
|
||||
from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\
|
||||
non_dead_end_crest_warps
|
||||
from .Items import item_table, item_groups, create_items, FFMQItem, fillers
|
||||
from .Output import generate_output
|
||||
from .Options import option_definitions
|
||||
from .Client import FFMQClient
|
||||
|
||||
|
||||
# removed until lists are supported
|
||||
# class FFMQSettings(settings.Group):
|
||||
# class APIUrls(list):
|
||||
# """A list of API URLs to get map shuffle, crest shuffle, and battlefield reward shuffle data from."""
|
||||
# api_urls: APIUrls = [
|
||||
# "https://api.ffmqrando.net/",
|
||||
# "http://ffmqr.jalchavware.com:5271/"
|
||||
# ]
|
||||
|
||||
|
||||
class FFMQWebWorld(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Final Fantasy Mystic Quest with Archipelago.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Alchav"]
|
||||
)]
|
||||
|
||||
|
||||
class FFMQWorld(World):
|
||||
"""Final Fantasy: Mystic Quest is a simple, humorous RPG for the Super Nintendo. You travel across four continents,
|
||||
linked in the middle of the world by the Focus Tower, which has been locked by four magical coins. Make your way to
|
||||
the bottom of the Focus Tower, then straight up through the top!"""
|
||||
# -Giga Otomia
|
||||
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None}
|
||||
location_name_to_id = location_table
|
||||
option_definitions = option_definitions
|
||||
|
||||
topology_present = True
|
||||
|
||||
item_name_groups = item_groups
|
||||
|
||||
generate_output = generate_output
|
||||
create_items = create_items
|
||||
create_regions = create_regions
|
||||
set_rules = set_rules
|
||||
stage_set_rules = stage_set_rules
|
||||
|
||||
data_version = 1
|
||||
|
||||
web = FFMQWebWorld()
|
||||
# settings: FFMQSettings
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
self.rom_name_available_event = threading.Event()
|
||||
self.rom_name = None
|
||||
self.rooms = None
|
||||
super().__init__(world, player)
|
||||
|
||||
def generate_early(self):
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
self.multiworld.brown_boxes[self.player].value = 1
|
||||
if self.multiworld.enemies_scaling_lower[self.player].value > \
|
||||
self.multiworld.enemies_scaling_upper[self.player].value:
|
||||
(self.multiworld.enemies_scaling_lower[self.player].value,
|
||||
self.multiworld.enemies_scaling_upper[self.player].value) =\
|
||||
(self.multiworld.enemies_scaling_upper[self.player].value,
|
||||
self.multiworld.enemies_scaling_lower[self.player].value)
|
||||
if self.multiworld.bosses_scaling_lower[self.player].value > \
|
||||
self.multiworld.bosses_scaling_upper[self.player].value:
|
||||
(self.multiworld.bosses_scaling_lower[self.player].value,
|
||||
self.multiworld.bosses_scaling_upper[self.player].value) =\
|
||||
(self.multiworld.bosses_scaling_upper[self.player].value,
|
||||
self.multiworld.bosses_scaling_lower[self.player].value)
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld):
|
||||
|
||||
# api_urls = Utils.get_options()["ffmq_options"].get("api_urls", None)
|
||||
api_urls = [
|
||||
"https://api.ffmqrando.net/",
|
||||
"http://ffmqr.jalchavware.com:5271/"
|
||||
]
|
||||
|
||||
rooms_data = {}
|
||||
|
||||
for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"):
|
||||
if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or
|
||||
world.multiworld.crest_shuffle[world.player]):
|
||||
if world.multiworld.map_shuffle_seed[world.player].value.isdigit():
|
||||
multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value))
|
||||
elif world.multiworld.map_shuffle_seed[world.player].value != "random":
|
||||
multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value))
|
||||
+ int(world.multiworld.seed))
|
||||
|
||||
seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()
|
||||
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}&cs={companion_shuffle}&km={kaeli_mom}"
|
||||
|
||||
if query in rooms_data:
|
||||
world.rooms = rooms_data[query]
|
||||
continue
|
||||
|
||||
if not api_urls:
|
||||
raise Exception("No FFMQR API URLs specified in host.yaml")
|
||||
|
||||
errors = []
|
||||
for api_url in api_urls.copy():
|
||||
try:
|
||||
response = requests.get(f"{api_url}GenerateRooms?{query}")
|
||||
except (ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ConnectionError,
|
||||
requests.exceptions.RequestException) as err:
|
||||
api_urls.remove(api_url)
|
||||
errors.append([api_url, err])
|
||||
else:
|
||||
if response.ok:
|
||||
world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader)
|
||||
break
|
||||
else:
|
||||
api_urls.remove(api_url)
|
||||
errors.append([api_url, response])
|
||||
else:
|
||||
error_text = f"Failed to fetch map shuffle data for FFMQ player {world.player}"
|
||||
for error in errors:
|
||||
error_text += f"\n{error[0]} - got error {error[1].status_code} {error[1].reason} {error[1].text}"
|
||||
raise Exception(error_text)
|
||||
api_urls.append(api_urls.pop(0))
|
||||
else:
|
||||
world.rooms = rooms
|
||||
|
||||
def create_item(self, name: str):
|
||||
return FFMQItem(name, self.player)
|
||||
|
||||
def collect_item(self, state, item, remove=False):
|
||||
if "Progressive" in item.name:
|
||||
i = item.code - 256
|
||||
if state.has(self.item_id_to_name[i], self.player):
|
||||
if state.has(self.item_id_to_name[i+1], self.player):
|
||||
return self.item_id_to_name[i+2]
|
||||
return self.item_id_to_name[i+1]
|
||||
return self.item_id_to_name[i]
|
||||
return item.name if item.advancement else None
|
||||
|
||||
def modify_multidata(self, multidata):
|
||||
# wait for self.rom_name to be available.
|
||||
self.rom_name_available_event.wait()
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
payload = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
multidata["connect_names"][new_name] = payload
|
||||
|
||||
def get_filler_item_name(self):
|
||||
r = self.multiworld.random.randint(0, 201)
|
||||
for item, count in fillers.items():
|
||||
r -= count
|
||||
r -= fillers[item]
|
||||
if r <= 0:
|
||||
return item
|
||||
|
||||
def extend_hint_information(self, hint_data):
|
||||
hint_data[self.player] = {}
|
||||
if self.multiworld.map_shuffle[self.player]:
|
||||
single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"]
|
||||
for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg",
|
||||
"Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship",
|
||||
"Subregion Doom Castle"]:
|
||||
region = self.multiworld.get_region(subregion, self.player)
|
||||
for location in region.locations:
|
||||
if location.address and self.multiworld.map_shuffle[self.player] != "dungeons":
|
||||
hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1]
|
||||
+ (" Region" if subregion not in
|
||||
single_location_regions else ""))
|
||||
for overworld_spot in region.exits:
|
||||
if ("Subregion" in overworld_spot.connected_region.name or
|
||||
overworld_spot.name == "Overworld - Mac Ship Doom" or "Focus Tower" in overworld_spot.name
|
||||
or "Doom Castle" in overworld_spot.name or overworld_spot.name == "Overworld - Giant Tree"):
|
||||
continue
|
||||
exits = list(overworld_spot.connected_region.exits) + [overworld_spot]
|
||||
checked_regions = set()
|
||||
while exits:
|
||||
exit_check = exits.pop()
|
||||
if (exit_check.connected_region not in checked_regions and "Subregion" not in
|
||||
exit_check.connected_region.name):
|
||||
checked_regions.add(exit_check.connected_region)
|
||||
exits.extend(exit_check.connected_region.exits)
|
||||
for location in exit_check.connected_region.locations:
|
||||
if location.address:
|
||||
hint = []
|
||||
if self.multiworld.map_shuffle[self.player] != "dungeons":
|
||||
hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not
|
||||
in single_location_regions else "")))
|
||||
if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \
|
||||
("Subregion Mac's Ship", "Subregion Doom Castle"):
|
||||
hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu",
|
||||
"Pazuzu's"))
|
||||
hint = " - ".join(hint)
|
||||
if location.address in hint_data[self.player]:
|
||||
hint_data[self.player][location.address] += f"/{hint}"
|
||||
else:
|
||||
hint_data[self.player][location.address] = hint
|
||||
|
||||
2450
worlds/ffmq/data/entrances.yaml
Normal file
2450
worlds/ffmq/data/entrances.yaml
Normal file
File diff suppressed because it is too large
Load Diff
4026
worlds/ffmq/data/rooms.yaml
Normal file
4026
worlds/ffmq/data/rooms.yaml
Normal file
File diff suppressed because it is too large
Load Diff
183
worlds/ffmq/data/settings.yaml
Normal file
183
worlds/ffmq/data/settings.yaml
Normal file
@@ -0,0 +1,183 @@
|
||||
# YAML Preset file for FFMQR
|
||||
Final Fantasy Mystic Quest:
|
||||
enemies_density:
|
||||
All: 0
|
||||
ThreeQuarter: 0
|
||||
Half: 0
|
||||
Quarter: 0
|
||||
None: 0
|
||||
chests_shuffle:
|
||||
Prioritize: 0
|
||||
Include: 0
|
||||
shuffle_boxes_content:
|
||||
true: 0
|
||||
false: 0
|
||||
npcs_shuffle:
|
||||
Prioritize: 0
|
||||
Include: 0
|
||||
Exclude: 0
|
||||
battlefields_shuffle:
|
||||
Prioritize: 0
|
||||
Include: 0
|
||||
Exclude: 0
|
||||
logic_options:
|
||||
Friendly: 0
|
||||
Standard: 0
|
||||
Expert: 0
|
||||
shuffle_enemies_position:
|
||||
true: 0
|
||||
false: 0
|
||||
enemies_scaling_lower:
|
||||
Quarter: 0
|
||||
Half: 0
|
||||
ThreeQuarter: 0
|
||||
Normal: 0
|
||||
OneAndQuarter: 0
|
||||
OneAndHalf: 0
|
||||
Double: 0
|
||||
DoubleAndHalf: 0
|
||||
Triple: 0
|
||||
enemies_scaling_upper:
|
||||
Quarter: 0
|
||||
Half: 0
|
||||
ThreeQuarter: 0
|
||||
Normal: 0
|
||||
OneAndQuarter: 0
|
||||
OneAndHalf: 0
|
||||
Double: 0
|
||||
DoubleAndHalf: 0
|
||||
Triple: 0
|
||||
bosses_scaling_lower:
|
||||
Quarter: 0
|
||||
Half: 0
|
||||
ThreeQuarter: 0
|
||||
Normal: 0
|
||||
OneAndQuarter: 0
|
||||
OneAndHalf: 0
|
||||
Double: 0
|
||||
DoubleAndHalf: 0
|
||||
Triple: 0
|
||||
bosses_scaling_upper:
|
||||
Quarter: 0
|
||||
Half: 0
|
||||
ThreeQuarter: 0
|
||||
Normal: 0
|
||||
OneAndQuarter: 0
|
||||
OneAndHalf: 0
|
||||
Double: 0
|
||||
DoubleAndHalf: 0
|
||||
Triple: 0
|
||||
enemizer_attacks:
|
||||
Normal: 0
|
||||
Safe: 0
|
||||
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
|
||||
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
|
||||
StandardExtended: 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
|
||||
Five: 0
|
||||
Three: 0
|
||||
One: 0
|
||||
RandomHigh: 0
|
||||
RandomLow: 0
|
||||
shuffle_battlefield_rewards:
|
||||
true: 0
|
||||
false: 0
|
||||
random_starting_weapon:
|
||||
true: 0
|
||||
false: 0
|
||||
progressive_gear:
|
||||
true: 0
|
||||
false: 0
|
||||
tweaked_dungeons:
|
||||
true: 0
|
||||
false: 0
|
||||
doom_castle_mode:
|
||||
Standard: 0
|
||||
BossRush: 0
|
||||
DarkKingOnly: 0
|
||||
doom_castle_shortcut:
|
||||
true: 0
|
||||
false: 0
|
||||
sky_coin_mode:
|
||||
Standard: 0
|
||||
StartWith: 0
|
||||
SaveTheCrystals: 0
|
||||
ShatteredSkyCoin: 0
|
||||
sky_coin_fragments_qty:
|
||||
Low16: 0
|
||||
Mid24: 0
|
||||
High32: 0
|
||||
RandomNarrow: 0
|
||||
RandomWide: 0
|
||||
enable_spoilers:
|
||||
true: 0
|
||||
false: 0
|
||||
progressive_formations:
|
||||
Disabled: 0
|
||||
RegionsStrict: 0
|
||||
RegionsKeepType: 0
|
||||
map_shuffling:
|
||||
None: 0
|
||||
Overworld: 0
|
||||
Dungeons: 0
|
||||
OverworldDungeons: 0
|
||||
Everything: 0
|
||||
crest_shuffle:
|
||||
true: 0
|
||||
false: 0
|
||||
description: Generated by Archipelago
|
||||
game: Final Fantasy Mystic Quest
|
||||
name: Player
|
||||
33
worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md
Normal file
33
worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Final Fantasy Mystic Quest
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Besides items being shuffled, you have multiple options for shuffling maps, crest warps, and battlefield locations.
|
||||
There are a number of other options for tweaking the difficulty of the game.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
Items received normally through chests, from NPCs, or battlefields are shuffled. Optionally, you may also include
|
||||
the items from brown boxes.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
|
||||
Any of the items which can be shuffled may also be placed into another player's world.
|
||||
|
||||
## What does another world's item look like in Final Fantasy Mystic Quest?
|
||||
|
||||
For locations that are originally boxes or chests, they will appear as a box if the item in it is categorized as a
|
||||
filler item, and a chest if it contains a useful or advancement item. Trap items may randomly appear as a box or chest.
|
||||
When opening a chest with an item for another player, you will see the Archipelago icon and it will tell you you've
|
||||
found an "Archipelago Item"
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
A dialogue box will open to show you the item you've received. You will not receive items while you are in battle,
|
||||
menus, or the overworld (except sometimes when closing the menu).
|
||||
|
||||
162
worlds/ffmq/docs/setup_en.md
Normal file
162
worlds/ffmq/docs/setup_en.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Final Fantasy Mystic Quest Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client`
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
- snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
|
||||
- RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
|
||||
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
|
||||
compatible hardware
|
||||
|
||||
- Your legally obtained Final Fantasy Mystic Quest 1.1 ROM file, probably named `Final Fantasy - Mystic Quest (U) (V1.1).sfc`
|
||||
The Archipelago community cannot supply you with this.
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Windows Setup
|
||||
|
||||
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
|
||||
or you are on an older version, you may run the installer again to install the SNI Client.
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
3. Check the box next to **Always use this app to open .sfc files**
|
||||
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
|
||||
extracted in step one.
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
|
||||
See the guide on setting up a basic YAML at the Archipelago setup
|
||||
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
|
||||
- Player Settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings)
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your `.apmq` patch file.
|
||||
5. Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest 1.1 ROM
|
||||
and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM.
|
||||
7. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your patch file, or with a zip file containing
|
||||
everyone's patch files. Your patch file should have a `.apmq` extension.
|
||||
|
||||
Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest 1.1 ROM
|
||||
and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM.
|
||||
|
||||
Manually launch the SNI Client, and run the patched ROM in your chosen software or hardware.
|
||||
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x-rr
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the connector lua file included with your client
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit.
|
||||
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
|
||||
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
|
||||
|
||||
##### BizHawk
|
||||
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
|
||||
menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`.
|
||||
5. Select the `Connector.lua` file included with your client
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
|
||||
|
||||
##### RetroArch 1.10.1 or newer
|
||||
|
||||
You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.1.
|
||||
|
||||
1. Enter the RetroArch main menu screen.
|
||||
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
|
||||
Network Command Port at 55355.
|
||||
|
||||

|
||||
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
|
||||
Performance)".
|
||||
|
||||
When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to
|
||||
read ROM data.
|
||||
|
||||
#### With hardware
|
||||
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
|
||||
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
|
||||
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
|
||||
|
||||
Other hardware may find helpful information on the usb2snes platforms
|
||||
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Power on your device and load the ROM.
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
|
||||
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
|
||||
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
|
||||
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
|
||||
into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||
|
||||
### Play the game
|
||||
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
|
||||
successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
|
||||
The recommended way to host a game is to use our hosting service. The process is relatively simple:
|
||||
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the Generate page above.
|
||||
- Generate page: [WebHost Seed Generation Page](/generate)
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
|
||||
they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
@@ -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,32 +1,29 @@
|
||||
import binascii
|
||||
import bsdiff4
|
||||
import os
|
||||
import pkgutil
|
||||
import settings
|
||||
import typing
|
||||
import tempfile
|
||||
import typing
|
||||
|
||||
import bsdiff4
|
||||
|
||||
import settings
|
||||
from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial
|
||||
from Fill import fill_restrictive
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .Common import *
|
||||
from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData,
|
||||
ladxr_item_to_la_item_name, links_awakening_items,
|
||||
links_awakening_items_by_name, ItemName)
|
||||
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
|
||||
ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name)
|
||||
from .LADXR import generator
|
||||
from .LADXR.itempool import ItemPool as LADXRItemPool
|
||||
from .LADXR.locations.constants import CHEST_ITEMS
|
||||
from .LADXR.locations.instrument import Instrument
|
||||
from .LADXR.logic import Logic as LAXDRLogic
|
||||
from .LADXR.main import get_parser
|
||||
from .LADXR.settings import Settings as LADXRSettings
|
||||
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
|
||||
from .LADXR.locations.instrument import Instrument
|
||||
from .LADXR.locations.constants import CHEST_ITEMS
|
||||
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
|
||||
create_regions_from_ladxr, get_locations_to_id)
|
||||
from .Options import links_awakening_options, DungeonItemShuffle
|
||||
|
||||
from .Options import DungeonItemShuffle, links_awakening_options
|
||||
from .Rom import LADXDeltaPatch
|
||||
|
||||
DEVELOPER_MODE = False
|
||||
@@ -511,16 +508,12 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
def collect(self, state, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change:
|
||||
rupees = self.rupees.get(item.name, 0)
|
||||
state.prog_items[item.player]["RUPEES"] += rupees
|
||||
|
||||
if change and item.name in self.rupees:
|
||||
state.prog_items[self.player]["RUPEES"] += self.rupees[item.name]
|
||||
return change
|
||||
|
||||
def remove(self, state, item: Item) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change:
|
||||
rupees = self.rupees.get(item.name, 0)
|
||||
state.prog_items[item.player]["RUPEES"] -= rupees
|
||||
|
||||
if change and item.name in self.rupees:
|
||||
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
|
||||
return change
|
||||
|
||||
38
worlds/ladx/test/testShop.py
Normal file
38
worlds/ladx/test/testShop.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import Optional
|
||||
|
||||
from Fill import distribute_planned
|
||||
from test.general import setup_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
from . import LADXTestBase
|
||||
from .. import LinksAwakeningWorld
|
||||
|
||||
|
||||
class PlandoTest(LADXTestBase):
|
||||
options = {
|
||||
"plando_items": [{
|
||||
"items": {
|
||||
"Progressive Sword": 2,
|
||||
},
|
||||
"locations": [
|
||||
"Shop 200 Item (Mabe Village)",
|
||||
"Shop 980 Item (Mabe Village)",
|
||||
],
|
||||
}],
|
||||
}
|
||||
|
||||
def world_setup(self, seed: Optional[int] = None) -> None:
|
||||
self.multiworld = setup_solo_multiworld(
|
||||
LinksAwakeningWorld,
|
||||
("generate_early", "create_regions", "create_items", "set_rules", "generate_basic")
|
||||
)
|
||||
self.multiworld.plando_items[1] = self.options["plando_items"]
|
||||
distribute_planned(self.multiworld)
|
||||
call_all(self.multiworld, "pre_fill")
|
||||
|
||||
def test_planned(self):
|
||||
"""Tests plandoing swords in the shop."""
|
||||
location_names = ["Shop 200 Item (Mabe Village)", "Shop 980 Item (Mabe Village)"]
|
||||
locations = [self.multiworld.get_location(loc, 1) for loc in location_names]
|
||||
for loc in locations:
|
||||
self.assertEqual("Progressive Sword", loc.item.name)
|
||||
self.assertFalse(loc.can_reach(self.multiworld.state))
|
||||
@@ -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
|
||||
@@ -49,6 +51,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 +104,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"
|
||||
|
||||
@@ -190,6 +190,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.
|
||||
@@ -369,11 +388,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 +414,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,7 +3,8 @@ from . import LingoTestBase
|
||||
|
||||
class TestRequiredRoomLogic(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_pilgrim_first(self) -> None:
|
||||
@@ -49,7 +50,8 @@ class TestRequiredRoomLogic(LingoTestBase):
|
||||
|
||||
class TestRequiredDoorLogic(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_through_rhyme(self) -> None:
|
||||
@@ -76,7 +78,8 @@ class TestRequiredDoorLogic(LingoTestBase):
|
||||
|
||||
class TestSimpleDoors(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "simple"
|
||||
"shuffle_doors": "simple",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
|
||||
@@ -81,7 +81,8 @@ class TestSimpleHallwayRoom(LingoTestBase):
|
||||
|
||||
class TestProgressiveArtGallery(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_colors": "false",
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -176,11 +176,14 @@ class MessengerWorld(World):
|
||||
self.total_shards += count
|
||||
return MessengerItem(name, self.player, item_id, override_prog, count)
|
||||
|
||||
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
|
||||
if item.advancement and "Time Shard" in item.name:
|
||||
shard_count = int(item.name.strip("Time Shard ()"))
|
||||
if remove:
|
||||
shard_count = -shard_count
|
||||
state.prog_items[self.player]["Shards"] += shard_count
|
||||
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change and "Time Shard" in item.name:
|
||||
state.prog_items[self.player]["Shards"] += int(item.name.strip("Time Shard ()"))
|
||||
return change
|
||||
|
||||
return super().collect_item(state, item, remove)
|
||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change and "Time Shard" in item.name:
|
||||
state.prog_items[self.player]["Shards"] -= int(item.name.strip("Time Shard ()"))
|
||||
return change
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -495,4 +495,10 @@ Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10|
|
||||
RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10|
|
||||
Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10|
|
||||
OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10|
|
||||
Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8|
|
||||
Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8|
|
||||
New York Back Raise|68-0|Gambler's Tricks|True|6|8|10|
|
||||
slic.hertz|68-1|Gambler's Tricks|True|5|7|9|
|
||||
Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11
|
||||
Swing Edge|68-3|Gambler's Tricks|True|4|8|10|
|
||||
Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11
|
||||
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
|
||||
31
worlds/musedash/Presets.py
Normal file
31
worlds/musedash/Presets.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
# An option to support Short Sync games. 40 songs.
|
||||
"No DLC - Short": {
|
||||
"allow_just_as_planned_dlc_songs": False,
|
||||
"starting_song_count": 5,
|
||||
"additional_song_count": 34,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
# An option to support Short Sync games but adds variety. 40 songs.
|
||||
"DLC - Short": {
|
||||
"allow_just_as_planned_dlc_songs": True,
|
||||
"starting_song_count": 5,
|
||||
"additional_song_count": 34,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
# An option to support Longer Sync/Async games. 100 songs.
|
||||
"DLC - Long": {
|
||||
"allow_just_as_planned_dlc_songs": True,
|
||||
"starting_song_count": 8,
|
||||
"additional_song_count": 91,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
}
|
||||
@@ -8,6 +8,7 @@ from .Options import MuseDashOptions
|
||||
from .Items import MuseDashSongItem, MuseDashFixedItem
|
||||
from .Locations import MuseDashLocation
|
||||
from .MuseDashCollection import MuseDashCollections
|
||||
from .Presets import MuseDashPresets
|
||||
|
||||
|
||||
class MuseDashWebWorld(WebWorld):
|
||||
@@ -33,6 +34,7 @@ class MuseDashWebWorld(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_es]
|
||||
options_presets = MuseDashPresets
|
||||
|
||||
|
||||
class MuseDashWorld(World):
|
||||
|
||||
@@ -254,7 +254,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
"key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "replace", "value": event_bitfield}]
|
||||
"operations": [{"operation": "or", "value": event_bitfield}]
|
||||
}])
|
||||
self.local_set_events = local_set_events
|
||||
|
||||
@@ -269,7 +269,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
"key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "replace", "value": key_bitfield}]
|
||||
"operations": [{"operation": "or", "value": key_bitfield}]
|
||||
}])
|
||||
self.local_found_key_items = local_found_key_items
|
||||
except bizhawk.RequestFailedError:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
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
|
||||
|
||||
@@ -146,6 +146,10 @@ sample_chao_names = [
|
||||
"Rin",
|
||||
"Doomguy",
|
||||
"Guide",
|
||||
"May",
|
||||
"Hubert",
|
||||
"Corvus",
|
||||
"Nigel",
|
||||
]
|
||||
|
||||
totally_real_item_names = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,30 +1,77 @@
|
||||
import typing
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Location
|
||||
from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \
|
||||
locBBH_table, \
|
||||
locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \
|
||||
locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \
|
||||
locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \
|
||||
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
|
||||
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
|
||||
|
||||
# List of all courses, including secrets, without BitS as that one is static
|
||||
sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt",
|
||||
"Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land",
|
||||
"Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride",
|
||||
"The Princess's Secret Slide", "The Secret Aquarium", "Bowser in the Dark World", "Tower of the Wing Cap",
|
||||
"Cavern of the Metal Cap", "Vanish Cap under the Moat", "Bowser in the Fire Sea", "Wing Mario over the Rainbow"]
|
||||
class SM64Levels(int, Enum):
|
||||
BOB_OMB_BATTLEFIELD = 91
|
||||
WHOMPS_FORTRESS = 241
|
||||
JOLLY_ROGER_BAY = 121
|
||||
COOL_COOL_MOUNTAIN = 51
|
||||
BIG_BOOS_HAUNT = 41
|
||||
HAZY_MAZE_CAVE = 71
|
||||
LETHAL_LAVA_LAND = 221
|
||||
SHIFTING_SAND_LAND = 81
|
||||
DIRE_DIRE_DOCKS = 231
|
||||
SNOWMANS_LAND = 101
|
||||
WET_DRY_WORLD = 111
|
||||
TALL_TALL_MOUNTAIN = 361
|
||||
TINY_HUGE_ISLAND_TINY = 132
|
||||
TINY_HUGE_ISLAND_HUGE = 131
|
||||
TICK_TOCK_CLOCK = 141
|
||||
RAINBOW_RIDE = 151
|
||||
THE_PRINCESS_SECRET_SLIDE = 271
|
||||
THE_SECRET_AQUARIUM = 201
|
||||
BOWSER_IN_THE_DARK_WORLD = 171
|
||||
TOWER_OF_THE_WING_CAP = 291
|
||||
CAVERN_OF_THE_METAL_CAP = 281
|
||||
VANISH_CAP_UNDER_THE_MOAT = 181
|
||||
BOWSER_IN_THE_FIRE_SEA = 191
|
||||
WING_MARIO_OVER_THE_RAINBOW = 311
|
||||
|
||||
# sm64paintings is list of entrances, format LEVEL | AREA. String Reference below
|
||||
sm64paintings = [91,241,121,51,41,71,221,81,231,101,111,361,132,131,141,151]
|
||||
sm64paintings_s = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"]
|
||||
# sm64secrets is list of secret areas
|
||||
sm64secrets = [271, 201, 171, 291, 281, 181, 191, 311]
|
||||
sm64secrets_s = ["PSS", "SA", "BitDW", "TOTWC", "COTMC", "VCUTM", "BitFS", "WMOTR"]
|
||||
# sm64paintings is a dict of entrances, format LEVEL | AREA
|
||||
sm64_level_to_paintings: typing.Dict[SM64Levels, str] = {
|
||||
SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield",
|
||||
SM64Levels.WHOMPS_FORTRESS: "Whomp's Fortress",
|
||||
SM64Levels.JOLLY_ROGER_BAY: "Jolly Roger Bay",
|
||||
SM64Levels.COOL_COOL_MOUNTAIN: "Cool, Cool Mountain",
|
||||
SM64Levels.BIG_BOOS_HAUNT: "Big Boo's Haunt",
|
||||
SM64Levels.HAZY_MAZE_CAVE: "Hazy Maze Cave",
|
||||
SM64Levels.LETHAL_LAVA_LAND: "Lethal Lava Land",
|
||||
SM64Levels.SHIFTING_SAND_LAND: "Shifting Sand Land",
|
||||
SM64Levels.DIRE_DIRE_DOCKS: "Dire, Dire Docks",
|
||||
SM64Levels.SNOWMANS_LAND: "Snowman's Land",
|
||||
SM64Levels.WET_DRY_WORLD: "Wet-Dry World",
|
||||
SM64Levels.TALL_TALL_MOUNTAIN: "Tall, Tall Mountain",
|
||||
SM64Levels.TINY_HUGE_ISLAND_TINY: "Tiny-Huge Island (Tiny)",
|
||||
SM64Levels.TINY_HUGE_ISLAND_HUGE: "Tiny-Huge Island (Huge)",
|
||||
SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock",
|
||||
SM64Levels.RAINBOW_RIDE: "Rainbow Ride"
|
||||
}
|
||||
sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() }
|
||||
|
||||
sm64entrances = sm64paintings + sm64secrets
|
||||
sm64entrances_s = sm64paintings_s + sm64secrets_s
|
||||
sm64_internalloc_to_string = dict(zip(sm64paintings+sm64secrets, sm64entrances_s))
|
||||
sm64_internalloc_to_regionid = dict(zip(sm64paintings+sm64secrets, list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets)))))
|
||||
# sm64secrets is a dict of secret areas, same format as sm64paintings
|
||||
sm64_level_to_secrets: typing.Dict[SM64Levels, str] = {
|
||||
SM64Levels.THE_PRINCESS_SECRET_SLIDE: "The Princess's Secret Slide",
|
||||
SM64Levels.THE_SECRET_AQUARIUM: "The Secret Aquarium",
|
||||
SM64Levels.BOWSER_IN_THE_DARK_WORLD: "Bowser in the Dark World",
|
||||
SM64Levels.TOWER_OF_THE_WING_CAP: "Tower of the Wing Cap",
|
||||
SM64Levels.CAVERN_OF_THE_METAL_CAP: "Cavern of the Metal Cap",
|
||||
SM64Levels.VANISH_CAP_UNDER_THE_MOAT: "Vanish Cap under the Moat",
|
||||
SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea",
|
||||
SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow"
|
||||
}
|
||||
sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() }
|
||||
|
||||
sm64_entrances_to_level = { **sm64_paintings_to_level, **sm64_secrets_to_level }
|
||||
sm64_level_to_entrances = { **sm64_level_to_paintings, **sm64_level_to_secrets }
|
||||
|
||||
def create_regions(world: MultiWorld, player: int):
|
||||
regSS = Region("Menu", player, world, "Castle Area")
|
||||
@@ -137,11 +184,13 @@ def create_regions(world: MultiWorld, player: int):
|
||||
regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM))
|
||||
world.regions.append(regTTM)
|
||||
|
||||
regTHI = create_region("Tiny-Huge Island", player, world)
|
||||
create_default_locs(regTHI, locTHI_table, player)
|
||||
regTHIT = create_region("Tiny-Huge Island (Tiny)", player, world)
|
||||
create_default_locs(regTHIT, locTHI_table, player)
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regTHI.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHI))
|
||||
world.regions.append(regTHI)
|
||||
regTHIT.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHIT))
|
||||
world.regions.append(regTHIT)
|
||||
regTHIH = create_region("Tiny-Huge Island (Huge)", player, world)
|
||||
world.regions.append(regTHIH)
|
||||
|
||||
regFloor3 = create_region("Third Floor", player, world)
|
||||
world.regions.append(regFloor3)
|
||||
|
||||
@@ -1,77 +1,86 @@
|
||||
from ..generic.Rules import add_rule
|
||||
from .Regions import connect_regions, sm64courses, sm64paintings, sm64secrets, sm64entrances
|
||||
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
|
||||
|
||||
def fix_reg(entrance_ids, reg, invalidspot, swaplist, world):
|
||||
if entrance_ids.index(reg) == invalidspot: # Unlucky :C
|
||||
swaplist.remove(invalidspot)
|
||||
rand = world.random.choice(swaplist)
|
||||
entrance_ids[invalidspot], entrance_ids[rand] = entrance_ids[rand], entrance_ids[invalidspot]
|
||||
swaplist.append(invalidspot)
|
||||
swaplist.remove(rand)
|
||||
def shuffle_dict_keys(world, obj: dict) -> dict:
|
||||
keys = list(obj.keys())
|
||||
values = list(obj.values())
|
||||
world.random.shuffle(keys)
|
||||
return dict(zip(keys,values))
|
||||
|
||||
def set_rules(world, player: int, area_connections):
|
||||
destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions
|
||||
secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
|
||||
course_entrance_ids = list(range(len(sm64paintings)))
|
||||
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
|
||||
world.random.shuffle(course_entrance_ids)
|
||||
def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set,
|
||||
swapdict: dict, world):
|
||||
if entrance_map[entrance] in invalid_regions: # Unlucky :C
|
||||
replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items()
|
||||
if rand_region not in invalid_regions]
|
||||
rand_region, rand_entrance = world.random.choice(replacement_regions)
|
||||
old_dest = entrance_map[entrance]
|
||||
entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest
|
||||
swapdict[rand_region] = entrance
|
||||
swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region
|
||||
|
||||
def set_rules(world, player: int, area_connections: dict):
|
||||
randomized_level_to_paintings = sm64_level_to_paintings.copy()
|
||||
randomized_level_to_secrets = sm64_level_to_secrets.copy()
|
||||
if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses
|
||||
randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings)
|
||||
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
|
||||
world.random.shuffle(secret_entrance_ids)
|
||||
entrance_ids = course_entrance_ids + secret_entrance_ids
|
||||
randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets)
|
||||
randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets }
|
||||
if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
|
||||
world.random.shuffle(entrance_ids)
|
||||
randomized_entrances = shuffle_dict_keys(world,randomized_entrances)
|
||||
swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() }
|
||||
# Guarantee first entrance is a course
|
||||
swaplist = list(range(len(entrance_ids)))
|
||||
if entrance_ids.index(0) > 15: # Unlucky :C
|
||||
rand = world.random.randint(0,15)
|
||||
entrance_ids[entrance_ids.index(0)], entrance_ids[rand] = entrance_ids[rand], entrance_ids[entrance_ids.index(0)]
|
||||
swaplist.remove(entrance_ids.index(0))
|
||||
# Guarantee COTMC is not mapped to HMC, cuz thats impossible
|
||||
fix_reg(entrance_ids, 20, 5, swaplist, world)
|
||||
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world)
|
||||
# Guarantee BITFS is not mapped to DDD
|
||||
fix_reg(entrance_ids, 22, 8, swaplist, world)
|
||||
if entrance_ids.index(22) == 5: # If BITFS is mapped to HMC...
|
||||
fix_reg(entrance_ids, 20, 8, swaplist, world) # ... then dont allow COTMC to be mapped to DDD
|
||||
temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only
|
||||
fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world)
|
||||
# Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
|
||||
if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
|
||||
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, world)
|
||||
else:
|
||||
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world)
|
||||
|
||||
# Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code
|
||||
area_connections.update({sm64entrances[entrance]: destination for entrance, destination in zip(entrance_ids,sm64entrances)})
|
||||
# Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise
|
||||
area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()})
|
||||
randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()}
|
||||
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) # BOB
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) # WF
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) # JRB
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) # CCM
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) # BBH
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[16]], lambda state: state.has("Power Star", player, 1)) # PSS
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[17]], lambda state: state.has("Power Star", player, 3)) # SA
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[19]], lambda state: state.has("Power Star", player, 10)) # TOTWC
|
||||
connect_regions(world, player, "Menu", sm64courses[temp_assign[18]], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) # BITDW
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"])
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["The Secret Aquarium"], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value))
|
||||
|
||||
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1))
|
||||
|
||||
connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) # HMC
|
||||
connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) # LLL
|
||||
connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) # SSL
|
||||
connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) # DDD
|
||||
connect_regions(world, player, "Hazy Maze Cave", sm64courses[temp_assign[20]]) # COTMC
|
||||
connect_regions(world, player, "Basement", sm64courses[temp_assign[21]]) # VCUTM
|
||||
connect_regions(world, player, "Basement", sm64courses[temp_assign[22]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
|
||||
state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) # BITFS
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value))
|
||||
connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
|
||||
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
|
||||
|
||||
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
|
||||
|
||||
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) # SL
|
||||
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) # WDW
|
||||
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) # TTM
|
||||
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny
|
||||
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Snowman's Land"])
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Wet-Dry World"])
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"])
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"])
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"])
|
||||
connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island (Huge)")
|
||||
connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island (Tiny)")
|
||||
|
||||
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value))
|
||||
|
||||
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) # TTC
|
||||
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) # RR
|
||||
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[23]]) # WMOTR
|
||||
connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) # BITS
|
||||
connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"])
|
||||
connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"])
|
||||
connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"])
|
||||
connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value))
|
||||
|
||||
#Special Rules for some Locations
|
||||
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player))
|
||||
|
||||
@@ -5,7 +5,7 @@ from .Items import item_table, cannon_item_table, SM64Item
|
||||
from .Locations import location_table, SM64Location
|
||||
from .Options import sm64_options
|
||||
from .Rules import set_rules
|
||||
from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internalloc_to_string, sm64_internalloc_to_regionid
|
||||
from .Regions import create_regions, sm64_level_to_entrances
|
||||
from BaseClasses import Item, Tutorial, ItemClassification
|
||||
from ..AutoWorld import World, WebWorld
|
||||
|
||||
@@ -55,8 +55,8 @@ class SM64World(World):
|
||||
# Write area_connections to spoiler log
|
||||
for entrance, destination in self.area_connections.items():
|
||||
self.multiworld.spoiler.set_entrance(
|
||||
sm64_internalloc_to_string[entrance] + " Entrance",
|
||||
sm64_internalloc_to_string[destination],
|
||||
sm64_level_to_entrances[entrance] + " Entrance",
|
||||
sm64_level_to_entrances[destination],
|
||||
'entrance', self.player)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
@@ -182,8 +182,7 @@ class SM64World(World):
|
||||
if self.topology_present:
|
||||
er_hint_data = {}
|
||||
for entrance, destination in self.area_connections.items():
|
||||
regionid = sm64_internalloc_to_regionid[destination]
|
||||
region = self.multiworld.get_region(sm64courses[regionid], self.player)
|
||||
region = self.multiworld.get_region(sm64_level_to_entrances[destination], self.player)
|
||||
for location in region.locations:
|
||||
er_hint_data[location.address] = sm64_internalloc_to_string[entrance]
|
||||
er_hint_data[location.address] = sm64_level_to_entrances[entrance]
|
||||
multidata['er_hint_data'][self.player] = er_hint_data
|
||||
|
||||
@@ -319,7 +319,7 @@ class Patch:
|
||||
def WriteZ3Locations(self, locations: List[Location]):
|
||||
for location in locations:
|
||||
if (location.Type == LocationType.HeraStandingKey):
|
||||
self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB]))
|
||||
self.patches.append((Snes(0x9E3BB), [0xEB]))
|
||||
elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]):
|
||||
text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something))
|
||||
if (location.Type == LocationType.Pedestal):
|
||||
|
||||
@@ -1536,6 +1536,7 @@ class StardewLogic:
|
||||
reach_west = self.can_reach_region(Region.island_west)
|
||||
reach_hut = self.can_reach_region(Region.leo_hut)
|
||||
reach_southeast = self.can_reach_region(Region.island_south_east)
|
||||
reach_field_office = self.can_reach_region(Region.field_office)
|
||||
reach_pirate_cove = self.can_reach_region(Region.pirate_cove)
|
||||
reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut)
|
||||
reach_volcano_regions = [self.can_reach_region(Region.volcano),
|
||||
@@ -1544,12 +1545,12 @@ class StardewLogic:
|
||||
self.can_reach_region(Region.volcano_floor_10)]
|
||||
reach_volcano = Or(reach_volcano_regions)
|
||||
reach_all_volcano = And(reach_volcano_regions)
|
||||
reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano]
|
||||
reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office]
|
||||
reach_caves = And(self.can_reach_region(Region.qi_walnut_room), self.can_reach_region(Region.dig_site),
|
||||
self.can_reach_region(Region.gourmand_frog_cave),
|
||||
self.can_reach_region(Region.colored_crystals_cave),
|
||||
self.can_reach_region(Region.shipwreck), self.has(Weapon.any_slingshot))
|
||||
reach_entire_island = And(reach_outside_areas, reach_all_volcano,
|
||||
reach_entire_island = And(reach_outside_areas, reach_field_office, reach_all_volcano,
|
||||
reach_caves, reach_southeast, reach_pirate_cove)
|
||||
if number <= 5:
|
||||
return Or(reach_south, reach_north, reach_west, reach_volcano)
|
||||
@@ -1563,7 +1564,8 @@ class StardewLogic:
|
||||
return reach_entire_island
|
||||
gems = [Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz]
|
||||
return reach_entire_island & self.has(Fruit.banana) & self.has(gems) & self.can_mine_perfectly() & \
|
||||
self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic)
|
||||
self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \
|
||||
self.can_complete_field_office()
|
||||
|
||||
def has_everything(self, all_progression_items: Set[str]) -> StardewRule:
|
||||
all_regions = [region.name for region in vanilla_regions]
|
||||
|
||||
@@ -117,6 +117,9 @@ def get_pool_core(world):
|
||||
else:
|
||||
possible_level_locations = [location for location in standard_level_locations
|
||||
if location not in level_locations[8]]
|
||||
for location in placed_items.keys():
|
||||
if location in possible_level_locations:
|
||||
possible_level_locations.remove(location)
|
||||
for level in range(1, 9):
|
||||
if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla:
|
||||
placed_items[f"Level {level} Triforce"] = fragment
|
||||
|
||||
@@ -257,7 +257,7 @@ Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - Tr
|
||||
158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser
|
||||
158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser
|
||||
158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser
|
||||
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser
|
||||
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Full Dots & Eraser
|
||||
158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots
|
||||
158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser
|
||||
|
||||
@@ -307,9 +307,9 @@ Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back -
|
||||
Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F:
|
||||
158155 - 0x38663 (Second Barrier Panel) - True - True
|
||||
Door - 0x3865F (Second Barrier) - 0x38663
|
||||
158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser
|
||||
158156 - 0x021B5 (Back First Row 1) - True - Stars & Eraser
|
||||
158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser
|
||||
158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser
|
||||
158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Eraser
|
||||
158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser
|
||||
158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser
|
||||
158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser
|
||||
@@ -427,10 +427,10 @@ Keep Tower (Keep) - Keep - 0x04F8F:
|
||||
158206 - 0x0361B (Tower Shortcut Panel) - True - True
|
||||
Door - 0x04F8F (Tower Shortcut) - 0x0361B
|
||||
158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True
|
||||
158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Rotated Shapers
|
||||
158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers
|
||||
Laser - 0x014BB (Laser) - 0x0360E | 0x03317
|
||||
159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True
|
||||
159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True
|
||||
159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 & 0x01BEA - True
|
||||
159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True
|
||||
159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True
|
||||
159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True
|
||||
@@ -516,13 +516,13 @@ Town Red Rooftop (Town):
|
||||
158607 - 0x17C71 (Rooftop Discard) - True - Triangles
|
||||
158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Black/White Squares
|
||||
158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares
|
||||
158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares & Dots
|
||||
158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares
|
||||
158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Black/White Squares & Dots
|
||||
158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Black/White Squares & Dots
|
||||
158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True
|
||||
|
||||
Town Wooden Rooftop (Town):
|
||||
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots
|
||||
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Shapers & Dots & Eraser & Full Dots
|
||||
|
||||
Town Church (Town):
|
||||
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
|
||||
@@ -740,7 +740,7 @@ Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underw
|
||||
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers
|
||||
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers
|
||||
158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Shapers
|
||||
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers
|
||||
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers & Shapers
|
||||
Door - 0x18482 (Blue Water Pump) - 0x00E3A
|
||||
159332 - 0x3365F (Boat EP) - 0x09DB8 - True
|
||||
159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True
|
||||
@@ -859,7 +859,7 @@ Treehouse Green Bridge (Treehouse) - Treehouse Green Bridge Front House - 0x17E6
|
||||
158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers
|
||||
158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers
|
||||
158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Stars + Same Colored Symbol
|
||||
158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Rotated Shapers
|
||||
158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Negative Shapers & Rotated Shapers
|
||||
158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers
|
||||
|
||||
Treehouse Green Bridge Front House (Treehouse):
|
||||
@@ -917,10 +917,10 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - True
|
||||
158416 - 0x09E78 (Left Row 3) - 0x09E75 - Dots & Shapers
|
||||
158417 - 0x09E79 (Left Row 4) - 0x09E78 - Shapers & Rotated Shapers
|
||||
158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares
|
||||
158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers
|
||||
158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers & Dots
|
||||
158420 - 0x09E6B (Left Row 7) - 0x09E6F - Dots
|
||||
158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry
|
||||
158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares & Stars
|
||||
158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares
|
||||
158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots
|
||||
158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers
|
||||
158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers
|
||||
@@ -933,7 +933,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
|
||||
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Colored Squares & Dots
|
||||
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
|
||||
158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers
|
||||
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry
|
||||
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares
|
||||
Door - 0x09FFB (Staircase Near) - 0x09FD8
|
||||
|
||||
Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8:
|
||||
@@ -1009,8 +1009,8 @@ Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
|
||||
158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers
|
||||
158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry
|
||||
158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers
|
||||
158472 - 0x32962 (First Floor Left) - True - Rotated Shapers
|
||||
158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol
|
||||
158472 - 0x32962 (First Floor Left) - True - Rotated Shapers & Shapers
|
||||
158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares
|
||||
158474 - 0x01A31 (First Floor Middle) - True - Colored Squares
|
||||
158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser
|
||||
158478 - 0x288EA (First Wooden Beam) - True - Rotated Shapers
|
||||
|
||||
@@ -161,7 +161,7 @@ joke_hints = [
|
||||
]
|
||||
|
||||
|
||||
def get_always_hint_items(world: "WitnessWorld"):
|
||||
def get_always_hint_items(world: "WitnessWorld") -> List[str]:
|
||||
always = [
|
||||
"Boat",
|
||||
"Caves Shortcuts",
|
||||
@@ -187,17 +187,17 @@ def get_always_hint_items(world: "WitnessWorld"):
|
||||
return always
|
||||
|
||||
|
||||
def get_always_hint_locations(_: "WitnessWorld"):
|
||||
return {
|
||||
def get_always_hint_locations(_: "WitnessWorld") -> List[str]:
|
||||
return [
|
||||
"Challenge Vault Box",
|
||||
"Mountain Bottom Floor Discard",
|
||||
"Theater Eclipse EP",
|
||||
"Shipwreck Couch EP",
|
||||
"Mountainside Cloud Cycle EP",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_priority_hint_items(world: "WitnessWorld"):
|
||||
def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
|
||||
priority = {
|
||||
"Caves Mountain Shortcut (Door)",
|
||||
"Caves Swamp Shortcut (Door)",
|
||||
@@ -246,11 +246,11 @@ def get_priority_hint_items(world: "WitnessWorld"):
|
||||
lasers.append("Desert Laser")
|
||||
priority.update(world.random.sample(lasers, 6))
|
||||
|
||||
return priority
|
||||
return sorted(priority)
|
||||
|
||||
|
||||
def get_priority_hint_locations(_: "WitnessWorld"):
|
||||
return {
|
||||
def get_priority_hint_locations(_: "WitnessWorld") -> List[str]:
|
||||
return [
|
||||
"Swamp Purple Underwater",
|
||||
"Shipwreck Vault Box",
|
||||
"Town RGB Room Left",
|
||||
@@ -264,7 +264,7 @@ def get_priority_hint_locations(_: "WitnessWorld"):
|
||||
"Tunnels Theater Flowers EP",
|
||||
"Boat Shipwreck Green EP",
|
||||
"Quarry Stoneworks Control Room Left",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]):
|
||||
@@ -365,8 +365,8 @@ def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item]
|
||||
remaining_hints = hint_amount - len(hints)
|
||||
priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2)))
|
||||
|
||||
prog_items_in_this_world = sorted(list(prog_items_in_this_world))
|
||||
locations_in_this_world = sorted(list(loc_in_this_world))
|
||||
prog_items_in_this_world = sorted(prog_items_in_this_world)
|
||||
locations_in_this_world = sorted(loc_in_this_world)
|
||||
|
||||
world.random.shuffle(prog_items_in_this_world)
|
||||
world.random.shuffle(locations_in_this_world)
|
||||
|
||||
@@ -115,6 +115,7 @@ class WitnessPlayerItems:
|
||||
# Adjust item classifications based on game settings.
|
||||
eps_shuffled = self._world.options.shuffle_EPs
|
||||
come_to_you = self._world.options.elevators_come_to_you
|
||||
difficulty = self._world.options.puzzle_randomization
|
||||
for item_name, item_data in self.item_data.items():
|
||||
if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)",
|
||||
"Monastery Shortcuts",
|
||||
@@ -130,10 +131,12 @@ class WitnessPlayerItems:
|
||||
"Monastery Laser Shortcut (Door)",
|
||||
"Orchard Second Gate (Door)",
|
||||
"Jungle Bamboo Laser Shortcut (Door)",
|
||||
"Keep Pressure Plates 2 Exit (Door)",
|
||||
"Caves Elevator Controls (Panel)"}:
|
||||
# Downgrade doors that don't gate progress.
|
||||
item_data.classification = ItemClassification.useful
|
||||
elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled):
|
||||
# PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary
|
||||
item_data.classification = ItemClassification.useful
|
||||
|
||||
# Build the mandatory item list.
|
||||
self._mandatory_items: Dict[str, int] = {}
|
||||
|
||||
@@ -70,15 +70,19 @@ class WitnessPlayerLogic:
|
||||
for items_option in these_items:
|
||||
all_options.add(items_option.union(dependentItem))
|
||||
|
||||
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved...
|
||||
if panel_hex != "0x28A0D":
|
||||
return frozenset(all_options)
|
||||
# ...except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
|
||||
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
|
||||
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
|
||||
# In the future, it would be wise to make a distinction between "power dependencies" and other dependencies.
|
||||
if any("0x28998" in option for option in these_panels):
|
||||
return frozenset(all_options)
|
||||
if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
|
||||
these_items = all_options
|
||||
|
||||
these_items = all_options
|
||||
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
|
||||
elif panel_hex == "0x1C349":
|
||||
these_items = all_options
|
||||
|
||||
# For any other door entity, we just return a set with the item that opens it & disregard power dependencies
|
||||
else:
|
||||
return frozenset(all_options)
|
||||
|
||||
disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES
|
||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"}
|
||||
@@ -371,7 +375,7 @@ class WitnessPlayerLogic:
|
||||
if lasers:
|
||||
adjustment_linesets_in_order.append(get_laser_shuffle())
|
||||
|
||||
if world.options.shuffle_EPs:
|
||||
if world.options.shuffle_EPs == "obelisk_sides":
|
||||
ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items()
|
||||
if ep_obj["entityType"] == "EP")
|
||||
|
||||
@@ -485,7 +489,7 @@ class WitnessPlayerLogic:
|
||||
self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
|
||||
|
||||
for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items():
|
||||
if event_hex in self.COMPLETELY_DISABLED_ENTITIES:
|
||||
if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
|
||||
continue
|
||||
self.EVENT_PANELS.add(event_hex)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class WitnessRegions:
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
|
||||
self.created_entrances[(source, target)].append(connection)
|
||||
self.created_entrances[source, target].append(connection)
|
||||
|
||||
# Register any necessary indirect connections
|
||||
mentioned_regions = {
|
||||
|
||||
@@ -66,8 +66,8 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi
|
||||
|
||||
|
||||
def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool:
|
||||
entrance_forward = regio.created_entrances[(source, target)]
|
||||
entrance_backward = regio.created_entrances[(source, target)]
|
||||
entrance_forward = regio.created_entrances[source, target]
|
||||
entrance_backward = regio.created_entrances[target, source]
|
||||
|
||||
return (
|
||||
any(entrance.can_reach(state) for entrance in entrance_forward)
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
Disabled Locations:
|
||||
0x0356B (Challenge Vault Box)
|
||||
0x04D75 (Vault Door)
|
||||
0x0A332 (Start Timer)
|
||||
0x0088E (Small Basic)
|
||||
0x00BAF (Big Basic)
|
||||
0x00BF3 (Square)
|
||||
0x00C09 (Maze Map)
|
||||
0x00CDB (Stars and Dots)
|
||||
0x0051F (Symmetry)
|
||||
0x00524 (Stars and Shapers)
|
||||
0x00CD4 (Big Basic 2)
|
||||
0x00CB9 (Choice Squares Right)
|
||||
0x00CA1 (Choice Squares Middle)
|
||||
0x00C80 (Choice Squares Left)
|
||||
0x00C68 (Choice Squares 2 Right)
|
||||
0x00C59 (Choice Squares 2 Middle)
|
||||
0x00C22 (Choice Squares 2 Left)
|
||||
0x034F4 (Maze Hidden 1)
|
||||
0x034EC (Maze Hidden 2)
|
||||
0x1C31A (Dots Pillar)
|
||||
0x1C319 (Squares Pillar)
|
||||
|
||||
@@ -33,6 +33,7 @@ class ZillionSettings(settings.Group):
|
||||
"""File name of the Zillion US rom"""
|
||||
description = "Zillion US ROM File"
|
||||
copy_to = "Zillion (UE) [!].sms"
|
||||
assert ZillionDeltaPatch.hash
|
||||
md5s = [ZillionDeltaPatch.hash]
|
||||
|
||||
class RomStart(str):
|
||||
@@ -70,9 +71,11 @@ class ZillionWorld(World):
|
||||
web = ZillionWebWorld()
|
||||
|
||||
options_dataclass = ZillionOptions
|
||||
options: ZillionOptions
|
||||
options: ZillionOptions # type: ignore
|
||||
|
||||
settings: typing.ClassVar[ZillionSettings] # type: ignore
|
||||
# these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486
|
||||
|
||||
settings: typing.ClassVar[ZillionSettings]
|
||||
topology_present = True # indicate if world type has any meaningful layout/pathing
|
||||
|
||||
# map names to their IDs
|
||||
|
||||
@@ -41,7 +41,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
|
||||
return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id)
|
||||
|
||||
|
||||
LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]]
|
||||
LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]]
|
||||
""" { hash: (cs.prog_items, accessible_locations) } """
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import cast
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
from worlds.zillion import ZillionWorld
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user