mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-03 17:03:43 -07:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97d6e80556 | ||
|
|
d5abadc6d0 | ||
|
|
d08d716966 | ||
|
|
3ee4be2e33 | ||
|
|
9172cc4925 | ||
|
|
7f03a86dee | ||
|
|
1603bab1da | ||
|
|
70aae514be | ||
|
|
5fa1185d6d | ||
|
|
3a2a584ad3 | ||
|
|
c42f53d64f | ||
|
|
450e0eacf4 | ||
|
|
aa40e811f1 | ||
|
|
af96f71190 | ||
|
|
9e4cb6ee33 | ||
|
|
5d0748983b | ||
|
|
c4981e4b91 | ||
|
|
3f36c436ad | ||
|
|
db456cbcf1 | ||
|
|
c0b8384319 | ||
|
|
13036539b7 | ||
|
|
5a2e477dba | ||
|
|
f003c7130f | ||
|
|
0558351a12 | ||
|
|
3f20bdaaa2 | ||
|
|
3bf367d630 | ||
|
|
706fc19ab4 | ||
|
|
4fe024041d | ||
|
|
7afbf8b45b | ||
|
|
e1fc44f4e0 | ||
|
|
21fbb545e8 | ||
|
|
6cd08ea8dc | ||
|
|
85efee1432 | ||
|
|
ba9974fe2a |
@@ -20,7 +20,7 @@ from urllib.parse import urlparse
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||||
from Utils import output_path, local_path, open_file
|
from Utils import output_path, local_path, open_file, get_cert_none_ssl_context, persistent_store
|
||||||
|
|
||||||
|
|
||||||
class AdjusterWorld(object):
|
class AdjusterWorld(object):
|
||||||
@@ -102,14 +102,15 @@ def main():
|
|||||||
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.music = not args.disablemusic
|
args.music = not args.disablemusic
|
||||||
if args.update_sprites:
|
|
||||||
run_sprite_update()
|
|
||||||
sys.exit()
|
|
||||||
# set up logger
|
# set up logger
|
||||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
||||||
args.loglevel]
|
args.loglevel]
|
||||||
logging.basicConfig(format='%(message)s', level=loglevel)
|
logging.basicConfig(format='%(message)s', level=loglevel)
|
||||||
|
|
||||||
|
if args.update_sprites:
|
||||||
|
run_sprite_update()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
if not os.path.isfile(args.rom):
|
if not os.path.isfile(args.rom):
|
||||||
adjustGUI()
|
adjustGUI()
|
||||||
else:
|
else:
|
||||||
@@ -118,7 +119,6 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
args, path = adjust(args=args)
|
args, path = adjust(args=args)
|
||||||
from Utils import persistent_store
|
|
||||||
if isinstance(args.sprite, Sprite):
|
if isinstance(args.sprite, Sprite):
|
||||||
args.sprite = args.sprite.name
|
args.sprite = args.sprite.name
|
||||||
persistent_store("adjuster", "last_settings_3", args)
|
persistent_store("adjuster", "last_settings_3", args)
|
||||||
@@ -224,7 +224,6 @@ def adjustGUI():
|
|||||||
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
|
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
|
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
|
||||||
from Utils import persistent_store
|
|
||||||
if isinstance(guiargs.sprite, Sprite):
|
if isinstance(guiargs.sprite, Sprite):
|
||||||
guiargs.sprite = guiargs.sprite.name
|
guiargs.sprite = guiargs.sprite.name
|
||||||
persistent_store("adjuster", "last_settings_3", guiargs)
|
persistent_store("adjuster", "last_settings_3", guiargs)
|
||||||
@@ -241,12 +240,16 @@ def adjustGUI():
|
|||||||
def run_sprite_update():
|
def run_sprite_update():
|
||||||
import threading
|
import threading
|
||||||
done = threading.Event()
|
done = threading.Event()
|
||||||
top = Tk()
|
try:
|
||||||
top.withdraw()
|
top = Tk()
|
||||||
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
except:
|
||||||
|
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
||||||
|
else:
|
||||||
|
top.withdraw()
|
||||||
|
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||||
while not done.isSet():
|
while not done.isSet():
|
||||||
top.update()
|
task.do_events()
|
||||||
print("Done updating sprites")
|
logging.info("Done updating sprites")
|
||||||
|
|
||||||
|
|
||||||
def update_sprites(task, on_finish=None):
|
def update_sprites(task, on_finish=None):
|
||||||
@@ -254,7 +257,7 @@ def update_sprites(task, on_finish=None):
|
|||||||
successful = True
|
successful = True
|
||||||
sprite_dir = local_path("data", "sprites", "alttpr")
|
sprite_dir = local_path("data", "sprites", "alttpr")
|
||||||
os.makedirs(sprite_dir, exist_ok=True)
|
os.makedirs(sprite_dir, exist_ok=True)
|
||||||
|
ctx = get_cert_none_ssl_context()
|
||||||
def finished():
|
def finished():
|
||||||
task.close_window()
|
task.close_window()
|
||||||
if on_finish:
|
if on_finish:
|
||||||
@@ -262,7 +265,7 @@ def update_sprites(task, on_finish=None):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
task.update_status("Downloading alttpr sprites list")
|
task.update_status("Downloading alttpr sprites list")
|
||||||
with urlopen('https://alttpr.com/sprites') as response:
|
with urlopen('https://alttpr.com/sprites', context=ctx) as response:
|
||||||
sprites_arr = json.loads(response.read().decode("utf-8"))
|
sprites_arr = json.loads(response.read().decode("utf-8"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
||||||
@@ -289,7 +292,7 @@ def update_sprites(task, on_finish=None):
|
|||||||
|
|
||||||
def dl(sprite_url, filename):
|
def dl(sprite_url, filename):
|
||||||
target = os.path.join(sprite_dir, filename)
|
target = os.path.join(sprite_dir, filename)
|
||||||
with urlopen(sprite_url) as response, open(target, 'wb') as out:
|
with urlopen(sprite_url, context=ctx) as response, open(target, 'wb') as out:
|
||||||
shutil.copyfileobj(response, out)
|
shutil.copyfileobj(response, out)
|
||||||
|
|
||||||
def rem(sprite):
|
def rem(sprite):
|
||||||
@@ -400,12 +403,39 @@ class BackgroundTaskProgress(BackgroundTask):
|
|||||||
def update_status(self, text):
|
def update_status(self, text):
|
||||||
self.queue_event(lambda: self.label_var.set(text))
|
self.queue_event(lambda: self.label_var.set(text))
|
||||||
|
|
||||||
|
def do_events(self):
|
||||||
|
self.parent.update()
|
||||||
|
|
||||||
# only call this in an event callback
|
# only call this in an event callback
|
||||||
def close_window(self):
|
def close_window(self):
|
||||||
self.stop()
|
self.stop()
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundTaskProgressNullWindow(BackgroundTask):
|
||||||
|
def __init__(self, code_to_run, *args):
|
||||||
|
super().__init__(None, code_to_run, *args)
|
||||||
|
|
||||||
|
def process_queue(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
event = self.queue.get_nowait()
|
||||||
|
event()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_events(self):
|
||||||
|
self.process_queue()
|
||||||
|
|
||||||
|
def update_status(self, text):
|
||||||
|
self.queue_event(lambda: logging.info(text))
|
||||||
|
|
||||||
|
def close_window(self):
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
def get_rom_frame(parent=None):
|
def get_rom_frame(parent=None):
|
||||||
romFrame = Frame(parent)
|
romFrame = Frame(parent)
|
||||||
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ atexit.register(input, "Press enter to exit.")
|
|||||||
|
|
||||||
# 1 or more digits followed by m or g, then optional b
|
# 1 or more digits followed by m or g, then optional b
|
||||||
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
|
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
|
||||||
forge_version = "1.17.1-37.0.109"
|
forge_version = "1.17.1-37.1.1"
|
||||||
|
|
||||||
|
|
||||||
def prompt_yes_no(prompt):
|
def prompt_yes_no(prompt):
|
||||||
@@ -35,12 +35,10 @@ def prompt_yes_no(prompt):
|
|||||||
def find_ap_randomizer_jar(forge_dir):
|
def find_ap_randomizer_jar(forge_dir):
|
||||||
mods_dir = os.path.join(forge_dir, 'mods')
|
mods_dir = os.path.join(forge_dir, 'mods')
|
||||||
if os.path.isdir(mods_dir):
|
if os.path.isdir(mods_dir):
|
||||||
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
|
|
||||||
for entry in os.scandir(mods_dir):
|
for entry in os.scandir(mods_dir):
|
||||||
match = ap_mod_re.match(entry.name)
|
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
|
||||||
if match:
|
logging.info(f"Found AP randomizer mod: {entry.name}")
|
||||||
logging.info(f"Found AP randomizer mod: {match.group()}")
|
return entry.name
|
||||||
return match.group()
|
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
os.mkdir(mods_dir)
|
os.mkdir(mods_dir)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Currently, the following games are supported:
|
|||||||
* Timespinner
|
* Timespinner
|
||||||
* Super Metroid
|
* Super Metroid
|
||||||
* Secret of Evermore
|
* Secret of Evermore
|
||||||
|
* Final Fantasy
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
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
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|||||||
@@ -1048,6 +1048,7 @@ async def game_watcher(ctx: Context):
|
|||||||
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
|
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
|
||||||
await snes_flush_writes(ctx)
|
await snes_flush_writes(ctx)
|
||||||
|
|
||||||
|
|
||||||
async def run_game(romfile):
|
async def run_game(romfile):
|
||||||
auto_start = Utils.get_options()["lttp_options"].get("rom_start", True)
|
auto_start = Utils.get_options()["lttp_options"].get("rom_start", True)
|
||||||
if auto_start is True:
|
if auto_start is True:
|
||||||
|
|||||||
2
Utils.py
2
Utils.py
@@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
|
|||||||
build: int
|
build: int
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.2.1"
|
__version__ = "0.2.2"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
from yaml import load, dump, safe_load
|
from yaml import load, dump, safe_load
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from WebHostLib import app as raw_app
|
|||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
from WebHostLib.models import db
|
from WebHostLib.models import db
|
||||||
from WebHostLib.autolauncher import autohost
|
from WebHostLib.autolauncher import autohost, autogen
|
||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||||
from WebHostLib.options import create as create_options_files
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
@@ -45,6 +45,8 @@ if __name__ == "__main__":
|
|||||||
create_options_files()
|
create_options_files()
|
||||||
if app.config["SELFLAUNCH"]:
|
if app.config["SELFLAUNCH"]:
|
||||||
autohost(app.config)
|
autohost(app.config)
|
||||||
|
if app.config["SELFGEN"]:
|
||||||
|
autogen(app.config)
|
||||||
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
|
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
|
||||||
if app.config["DEBUG"]:
|
if app.config["DEBUG"]:
|
||||||
autohost(app.config)
|
autohost(app.config)
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ Pony(app)
|
|||||||
app.jinja_env.filters['any'] = any
|
app.jinja_env.filters['any'] = any
|
||||||
app.jinja_env.filters['all'] = all
|
app.jinja_env.filters['all'] = all
|
||||||
|
|
||||||
app.config["SELFHOST"] = True
|
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||||
app.config["SELFLAUNCH"] = True
|
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
||||||
|
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||||
app.config["DEBUG"] = False
|
app.config["DEBUG"] = False
|
||||||
app.config["PORT"] = 80
|
app.config["PORT"] = 80
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
@@ -166,8 +167,9 @@ def host_room(room: UUID):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if room.owner == session["_id"]:
|
if room.owner == session["_id"]:
|
||||||
cmd = request.form["cmd"]
|
cmd = request.form["cmd"]
|
||||||
Command(room=room, commandtext=cmd)
|
if cmd:
|
||||||
commit()
|
Command(room=room, commandtext=cmd)
|
||||||
|
commit()
|
||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
||||||
|
|||||||
@@ -110,6 +110,26 @@ def autohost(config: dict):
|
|||||||
def keep_running():
|
def keep_running():
|
||||||
try:
|
try:
|
||||||
with Locker("autohost"):
|
with Locker("autohost"):
|
||||||
|
while 1:
|
||||||
|
time.sleep(0.1)
|
||||||
|
with db_session:
|
||||||
|
rooms = select(
|
||||||
|
room for room in Room if
|
||||||
|
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
||||||
|
for room in rooms:
|
||||||
|
launch_room(room, config)
|
||||||
|
|
||||||
|
except AlreadyRunningException:
|
||||||
|
logging.info("Autohost reports as already running, not starting another.")
|
||||||
|
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=keep_running, name="AP_Autohost").start()
|
||||||
|
|
||||||
|
|
||||||
|
def autogen(config: dict):
|
||||||
|
def keep_running():
|
||||||
|
try:
|
||||||
|
with Locker("autogen"):
|
||||||
|
|
||||||
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
||||||
initargs=(config["PONY"],)) as generator_pool:
|
initargs=(config["PONY"],)) as generator_pool:
|
||||||
@@ -129,22 +149,17 @@ def autohost(config: dict):
|
|||||||
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
time.sleep(0.50)
|
time.sleep(0.1)
|
||||||
with db_session:
|
with db_session:
|
||||||
rooms = select(
|
|
||||||
room for room in Room if
|
|
||||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
|
||||||
for room in rooms:
|
|
||||||
launch_room(room, config)
|
|
||||||
to_start = select(
|
to_start = select(
|
||||||
generation for generation in Generation if generation.state == STATE_QUEUED)
|
generation for generation in Generation if generation.state == STATE_QUEUED)
|
||||||
for generation in to_start:
|
for generation in to_start:
|
||||||
launch_generator(generator_pool, generation)
|
launch_generator(generator_pool, generation)
|
||||||
except AlreadyRunningException:
|
except AlreadyRunningException:
|
||||||
pass
|
logging.info("Autogen reports as already running, not starting another.")
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
threading.Thread(target=keep_running).start()
|
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
||||||
|
|
||||||
|
|
||||||
multiworlds = {}
|
multiworlds = {}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ def update_sprites_lttp():
|
|||||||
from tkinter import Tk
|
from tkinter import Tk
|
||||||
from LttPAdjuster import get_image_for_sprite
|
from LttPAdjuster import get_image_for_sprite
|
||||||
from LttPAdjuster import BackgroundTaskProgress
|
from LttPAdjuster import BackgroundTaskProgress
|
||||||
|
from LttPAdjuster import BackgroundTaskProgressNullWindow
|
||||||
from LttPAdjuster import update_sprites
|
from LttPAdjuster import update_sprites
|
||||||
|
|
||||||
# Target directories
|
# Target directories
|
||||||
@@ -19,11 +20,15 @@ def update_sprites_lttp():
|
|||||||
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
||||||
# update sprites through gui.py's functions
|
# update sprites through gui.py's functions
|
||||||
done = threading.Event()
|
done = threading.Event()
|
||||||
top = Tk()
|
try:
|
||||||
top.withdraw()
|
top = Tk()
|
||||||
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
except:
|
||||||
|
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
||||||
|
else:
|
||||||
|
top.withdraw()
|
||||||
|
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||||
while not done.isSet():
|
while not done.isSet():
|
||||||
top.update()
|
task.do_events()
|
||||||
|
|
||||||
spriteData = []
|
spriteData = []
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,9 @@ locations. So ,for example, Princess Sarah may have the CANOE instead of the LUT
|
|||||||
Pot or some armor. There are plenty of other things that can be randomized on our
|
Pot or some armor. There are plenty of other things that can be randomized on our
|
||||||
[main randomizer site](https://finalfantasyrandomizer.com/)
|
[main randomizer site](https://finalfantasyrandomizer.com/)
|
||||||
|
|
||||||
Some features are not currently supported by AP. A non-exhaustive list includes:
|
|
||||||
- Shard Hunt
|
|
||||||
- Deep Dungeon
|
|
||||||
|
|
||||||
## What Final Fantasy items can appear in other players' worlds?
|
## What Final Fantasy items can appear in other players' worlds?
|
||||||
Currently, only progression items can appear in other players' worlds. Armor, Weapons and Consumable Items can not.
|
All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course,
|
||||||
|
key items.
|
||||||
|
|
||||||
## What does another world's item look like in Final Fantasy
|
## What does another world's item look like in Final Fantasy
|
||||||
All local and remote items appear the same. It will say that you received an item and then BOTH the client log and
|
All local and remote items appear the same. It will say that you received an item and then BOTH the client log and
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ so the game is always able to be completed. However, because of the item shuffle
|
|||||||
areas before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any
|
areas before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any
|
||||||
weapon is obtained.
|
weapon is obtained.
|
||||||
|
|
||||||
Additional help can be found in the [guide](https://github.com/black-sliver/evermizer/blob/feat-mw/guide.md).
|
Additional help can be found in the [guide](https://github.com/black-sliver/evermizer/blob/master/guide.md).
|
||||||
|
|
||||||
## What items and locations get shuffled?
|
## What items and locations get shuffled?
|
||||||
All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells
|
All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells
|
||||||
|
|||||||
49
WebHostLib/static/assets/supermetroidTracker.js
Normal file
49
WebHostLib/static/assets/supermetroidTracker.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Reload tracker every 15 seconds
|
||||||
|
const url = window.location;
|
||||||
|
setInterval(() => {
|
||||||
|
const ajax = new XMLHttpRequest();
|
||||||
|
ajax.onreadystatechange = () => {
|
||||||
|
if (ajax.readyState !== 4) { return; }
|
||||||
|
|
||||||
|
// Create a fake DOM using the returned HTML
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||||
|
|
||||||
|
// Update item tracker
|
||||||
|
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||||
|
// Update only counters in the location-table
|
||||||
|
let counters = document.getElementsByClassName('counter');
|
||||||
|
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||||
|
for (let i = 0; i < counters.length; i++) {
|
||||||
|
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ajax.open('GET', url);
|
||||||
|
ajax.send();
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
// Collapsible advancement sections
|
||||||
|
const categories = document.getElementsByClassName("location-category");
|
||||||
|
for (let i = 0; i < categories.length; i++) {
|
||||||
|
let hide_id = categories[i].id.split('-')[0];
|
||||||
|
if (hide_id == 'Total') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
categories[i].addEventListener('click', function() {
|
||||||
|
// Toggle the advancement list
|
||||||
|
document.getElementById(hide_id).classList.toggle("hide");
|
||||||
|
// Change text of the header
|
||||||
|
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||||
|
const orig_text = tab_header.innerHTML;
|
||||||
|
let new_text;
|
||||||
|
if (orig_text.includes("▼")) {
|
||||||
|
new_text = orig_text.replace("▼", "▲");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new_text = orig_text.replace("▲", "▼");
|
||||||
|
}
|
||||||
|
tab_header.innerHTML = new_text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -76,7 +76,7 @@ that can be rolled by these settings. If a game can be rolled it **must** have a
|
|||||||
|
|
||||||
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
||||||
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`,
|
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`,
|
||||||
`exclude_locations`, and various [plando options](tutorial/archipelago/plando/en).
|
`exclude_locations`, and various [plando options](/tutorial/archipelago/plando/en).
|
||||||
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
||||||
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
||||||
will give you 30 rupees.
|
will give you 30 rupees.
|
||||||
|
|||||||
104
WebHostLib/static/styles/supermetroidTracker.css
Normal file
104
WebHostLib/static/styles/supermetroidTracker.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#player-tracker-wrapper{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table{
|
||||||
|
border-top: 2px solid #000000;
|
||||||
|
border-left: 2px solid #000000;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
padding: 3px 3px 10px;
|
||||||
|
width: 384px;
|
||||||
|
background-color: #546E7A;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table td{
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table img{
|
||||||
|
height: 100%;
|
||||||
|
max-width: 40px;
|
||||||
|
max-height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table img.acquired{
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.counted-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.item-count {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
font-family: "Minecraftia", monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table{
|
||||||
|
width: 384px;
|
||||||
|
border-left: 2px solid #000000;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
background-color: #546E7A;
|
||||||
|
color: #000000;
|
||||||
|
padding: 0 3px 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table th{
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td{
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td.counter {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td.toggle-arrow {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table tr#Total-header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table img{
|
||||||
|
height: 100%;
|
||||||
|
max-width: 30px;
|
||||||
|
max-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table tbody.locations {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td.location-name {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
85
WebHostLib/templates/supermetroidTracker.html
Normal file
85
WebHostLib/templates/supermetroidTracker.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ player_name }}'s Tracker</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/supermetroidTracker.css') }}"/>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/supermetroidTracker.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
|
<table id="inventory-table">
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Charge Beam'] }}" class="{{ 'acquired' if 'Charge Beam' in acquired_items }}" title="Charge Beam" /></td>
|
||||||
|
<td><img src="{{ icons['Ice Beam'] }}" class="{{ 'acquired' if 'Ice Beam' in acquired_items }}" title="Ice Beam" /></td>
|
||||||
|
<td><img src="{{ icons['Wave Beam'] }}" class="{{ 'acquired' if 'Wave Beam' in acquired_items }}" title="Wave Beam" /></td>
|
||||||
|
<td><img src="{{ icons['Spazer'] }}" class="{{ 'acquired' if 'Spazer' in acquired_items }}" title="S p a z e r" /></td>
|
||||||
|
<td><img src="{{ icons['Plasma Beam'] }}" class="{{ 'acquired' if 'Plasma Beam' in acquired_items }}" title="Plasma Beam" /></td>
|
||||||
|
<td><img src="{{ icons['Varia Suit'] }}" class="{{ 'acquired' if 'Varia Suit' in acquired_items }}" title="Varia Suit" /></td>
|
||||||
|
<td><img src="{{ icons['Gravity Suit'] }}" class="{{ 'acquired' if 'Gravity Suit' in acquired_items }}" title="Gravity Suit" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Morph Ball'] }}" class="{{ 'acquired' if 'Morph Ball' in acquired_items }}" title="Morph Ball" /></td>
|
||||||
|
<td><img src="{{ icons['Bomb'] }}" class="{{ 'acquired' if 'Bomb' in acquired_items }}" title="Bomb" /></td>
|
||||||
|
<td><img src="{{ icons['Spring Ball'] }}" class="{{ 'acquired' if 'Spring Ball' in acquired_items }}" title="Spring Ball" /></td>
|
||||||
|
<td><img src="{{ icons['Screw Attack'] }}" class="{{ 'acquired' if 'Screw Attack' in acquired_items }}" title="Screw Attack" /></td>
|
||||||
|
<td><img src="{{ icons['Hi-Jump Boots'] }}" class="{{ 'acquired' if 'Hi-Jump Boots' in acquired_items }}" title="Hi-Jump Boots" /></td>
|
||||||
|
<td><img src="{{ icons['Space Jump'] }}" class="{{ 'acquired' if 'Space Jump' in acquired_items }}" title="Space Jump" /></td>
|
||||||
|
<td><img src="{{ icons['Speed Booster'] }}" class="{{ 'acquired' if 'Speed Booster' in acquired_items }}" title="Speed Booster" /></td>
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="counted-item">
|
||||||
|
<img src="{{ icons['Energy Tank'] }}" class="{{ 'acquired' if energy_count > 0 }}" title="Energy Tank" />
|
||||||
|
<div class="item-count">{{ energy_count }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="counted-item">
|
||||||
|
<img src="{{ icons['Reserve Tank'] }}" class="{{ 'acquired' if reserve_count > 0 }}" title="Reserve Tank" />
|
||||||
|
<div class="item-count">{{ reserve_count }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="counted-item">
|
||||||
|
<img src="{{ icons['Missile'] }}" class="{{ 'acquired' if missile_count > 0 }}" title="Missile ({{missile_count * 5}})" />
|
||||||
|
<div class="item-count">{{ missile_count }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="counted-item">
|
||||||
|
<img src="{{ icons['Super Missile'] }}" class="{{ 'acquired' if super_count > 0 }}" title="Super Missile ({{super_count * 5}})" />
|
||||||
|
<div class="item-count">{{ super_count }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="counted-item">
|
||||||
|
<img src="{{ icons['Power Bomb'] }}" class="{{ 'acquired' if power_count > 0 }}" title="Power Bomb ({{power_count * 5}})" />
|
||||||
|
<div class="item-count">{{ power_count }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><img src="{{ icons['Grappling Beam'] }}" class="{{ 'acquired' if 'Grappling Beam' in acquired_items }}" title="Grappling Beam" /></td>
|
||||||
|
<td><img src="{{ icons['X-Ray Scope'] }}" class="{{ 'acquired' if 'X-Ray Scope' in acquired_items }}" title="X-Ray Scope" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table id="location-table">
|
||||||
|
{% for area in checks_done %}
|
||||||
|
<tr class="location-category" id="{{area}}-header">
|
||||||
|
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||||
|
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||||
|
</tr>
|
||||||
|
<tbody class="locations hide" id="{{area}}">
|
||||||
|
{% for location in location_info[area] %}
|
||||||
|
<tr>
|
||||||
|
<td class="location-name">{{ location }}</td>
|
||||||
|
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -774,6 +774,106 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
|||||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||||
**display_data)
|
**display_data)
|
||||||
|
|
||||||
|
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||||
|
inventory: Counter, team: int, player: int, playerName: str,
|
||||||
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
|
||||||
|
|
||||||
|
icons = {
|
||||||
|
"Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ETank.png",
|
||||||
|
"Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Missile.png",
|
||||||
|
"Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Super.png",
|
||||||
|
"Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/PowerBomb.png",
|
||||||
|
"Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Bomb.png",
|
||||||
|
"Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Charge.png",
|
||||||
|
"Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Ice.png",
|
||||||
|
"Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/HiJump.png",
|
||||||
|
"Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpeedBooster.png",
|
||||||
|
"Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Wave.png",
|
||||||
|
"Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Spazer.png",
|
||||||
|
"Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpringBall.png",
|
||||||
|
"Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Varia.png",
|
||||||
|
"Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Plasma.png",
|
||||||
|
"Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Grapple.png",
|
||||||
|
"Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Morph.png",
|
||||||
|
"Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Reserve.png",
|
||||||
|
"Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Gravity.png",
|
||||||
|
"X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/XRayScope.png",
|
||||||
|
"Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpaceJump.png",
|
||||||
|
"Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ScrewAttack.png",
|
||||||
|
"Nothing": "",
|
||||||
|
"No Energy": "",
|
||||||
|
"Kraid": "",
|
||||||
|
"Phantoon": "",
|
||||||
|
"Draygon": "",
|
||||||
|
"Ridley": "",
|
||||||
|
"Mother Brain": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
multi_items = {
|
||||||
|
"Energy Tank": 83000,
|
||||||
|
"Missile": 83001,
|
||||||
|
"Super Missile": 83002,
|
||||||
|
"Power Bomb": 83003,
|
||||||
|
"Reserve Tank": 83020,
|
||||||
|
}
|
||||||
|
|
||||||
|
supermetroid_location_ids = {
|
||||||
|
'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029,
|
||||||
|
82000, 82004, 82006, 82009, 82010,
|
||||||
|
82011, 82012, 82027, 82028, 82034,
|
||||||
|
82036, 82037],
|
||||||
|
'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035,
|
||||||
|
82013, 82014, 82015, 82016, 82018,
|
||||||
|
82019, 82021, 82022, 82024, 82025,
|
||||||
|
82031],
|
||||||
|
'Red Brinstar': [82038, 82042, 82039, 82040, 82041],
|
||||||
|
'Kraid': [82043, 82048, 82044],
|
||||||
|
'Norfair': [82050, 82053, 82061, 82066, 82068,
|
||||||
|
82049, 82051, 82054, 82055, 82056,
|
||||||
|
82062, 82063, 82064, 82065, 82067],
|
||||||
|
'Lower Norfair': [82078, 82079, 82080, 82070, 82071,
|
||||||
|
82073, 82074, 82075, 82076, 82077],
|
||||||
|
'Crocomire': [82052, 82060, 82057, 82058, 82059],
|
||||||
|
'Wrecked Ship': [82129, 82132, 82134, 82135, 82001,
|
||||||
|
82002, 82003, 82128, 82130, 82131,
|
||||||
|
82133],
|
||||||
|
'West Maridia': [82138, 82136, 82137, 82139, 82140,
|
||||||
|
82141, 82142],
|
||||||
|
'East Maridia': [82143, 82145, 82150, 82152, 82154,
|
||||||
|
82144, 82146, 82147, 82148, 82149,
|
||||||
|
82151],
|
||||||
|
}
|
||||||
|
|
||||||
|
display_data = {}
|
||||||
|
|
||||||
|
|
||||||
|
for item_name, item_id in multi_items.items():
|
||||||
|
base_name = item_name.split()[0].lower()
|
||||||
|
count = inventory[item_id]
|
||||||
|
display_data[base_name+"_count"] = inventory[item_id]
|
||||||
|
|
||||||
|
# Victory condition
|
||||||
|
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||||
|
display_data['game_finished'] = game_state == 30
|
||||||
|
|
||||||
|
# Turn location IDs into advancement tab counts
|
||||||
|
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||||
|
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||||
|
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||||
|
for tab_name, tab_locations in supermetroid_location_ids.items()}
|
||||||
|
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||||
|
for tab_name, tab_locations in supermetroid_location_ids.items()}
|
||||||
|
checks_done['Total'] = len(checked_locations)
|
||||||
|
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
|
||||||
|
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||||
|
|
||||||
|
return render_template("supermetroidTracker.html",
|
||||||
|
inventory=inventory, icons=icons,
|
||||||
|
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||||
|
id in lookup_any_item_id_to_name},
|
||||||
|
player=player, team=team, room=room, player_name=playerName,
|
||||||
|
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||||
|
**display_data)
|
||||||
|
|
||||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
|
||||||
inventory: Counter, team: int, player: int, playerName: str,
|
inventory: Counter, team: int, player: int, playerName: str,
|
||||||
@@ -887,5 +987,6 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
|||||||
"Minecraft": __renderMinecraftTracker,
|
"Minecraft": __renderMinecraftTracker,
|
||||||
"Ocarina of Time": __renderOoTTracker,
|
"Ocarina of Time": __renderOoTTracker,
|
||||||
"Timespinner": __renderTimespinnerTracker,
|
"Timespinner": __renderTimespinnerTracker,
|
||||||
"A Link to the Past": __renderAlttpTracker
|
"A Link to the Past": __renderAlttpTracker,
|
||||||
|
"Super Metroid": __renderSuperMetroidTracker
|
||||||
}
|
}
|
||||||
9
kvui.py
9
kvui.py
@@ -8,11 +8,16 @@ os.environ["KIVY_NO_FILELOG"] = "1"
|
|||||||
os.environ["KIVY_NO_ARGS"] = "1"
|
os.environ["KIVY_NO_ARGS"] = "1"
|
||||||
os.environ["KIVY_LOG_ENABLE"] = "0"
|
os.environ["KIVY_LOG_ENABLE"] = "0"
|
||||||
|
|
||||||
|
from kivy.base import Config
|
||||||
|
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||||
|
Config.set('kivy', 'exit_on_escape', '0')
|
||||||
|
Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.core.clipboard import Clipboard
|
from kivy.core.clipboard import Clipboard
|
||||||
from kivy.core.text.markup import MarkupLabel
|
from kivy.core.text.markup import MarkupLabel
|
||||||
from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock
|
from kivy.base import ExceptionHandler, ExceptionManager, Clock
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty
|
from kivy.properties import BooleanProperty, ObjectProperty
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
@@ -431,6 +436,4 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
|||||||
|
|
||||||
ExceptionManager.add_handler(E())
|
ExceptionManager.add_handler(E())
|
||||||
|
|
||||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
|
||||||
Config.set('kivy', 'exit_on_escape', '0')
|
|
||||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ colorama>=0.4.4
|
|||||||
websockets>=10.1
|
websockets>=10.1
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0
|
||||||
fuzzywuzzy>=0.18.0
|
fuzzywuzzy>=0.18.0
|
||||||
appdirs>=1.4.4
|
|
||||||
jinja2>=3.0.3
|
jinja2>=3.0.3
|
||||||
schema>=0.7.4
|
schema>=0.7.4
|
||||||
|
kivy>=2.0.0
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -141,7 +141,7 @@ for folder in sdl2.dep_bins + glew.dep_bins:
|
|||||||
shutil.copytree(folder, libfolder, dirs_exist_ok=True)
|
shutil.copytree(folder, libfolder, dirs_exist_ok=True)
|
||||||
print('copying', folder, '->', libfolder)
|
print('copying', folder, '->', libfolder)
|
||||||
|
|
||||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
|
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
|
||||||
|
|
||||||
for data in extra_data:
|
for data in extra_data:
|
||||||
installfile(Path(data))
|
installfile(Path(data))
|
||||||
@@ -155,6 +155,7 @@ for worldname, worldtype in AutoWorldRegister.world_types.items():
|
|||||||
file_name = worldname+".yaml"
|
file_name = worldname+".yaml"
|
||||||
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
|
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
|
||||||
buildfolder / "Players" / "Templates" / file_name)
|
buildfolder / "Players" / "Templates" / file_name)
|
||||||
|
shutil.copyfile("meta.yaml", buildfolder / "Players" / "Templates" / "meta.yaml")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from maseya import z3pr
|
from maseya import z3pr
|
||||||
|
|||||||
@@ -2287,7 +2287,7 @@ def write_strings(rom, world, player):
|
|||||||
if hint_count:
|
if hint_count:
|
||||||
locations = world.find_items_in_locations(set(items_to_hint), player)
|
locations = world.find_items_in_locations(set(items_to_hint), player)
|
||||||
local_random.shuffle(locations)
|
local_random.shuffle(locations)
|
||||||
for x in range(hint_count):
|
for x in range(min(hint_count, len(locations))):
|
||||||
this_location = locations.pop()
|
this_location = locations.pop()
|
||||||
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
|
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
|
||||||
tt[hint_locations.pop(0)] = this_hint
|
tt[hint_locations.pop(0)] = this_hint
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class TechTreeLayout(Choice):
|
|||||||
option_small_funnels = 7
|
option_small_funnels = 7
|
||||||
option_medium_funnels = 8
|
option_medium_funnels = 8
|
||||||
option_large_funnels = 9
|
option_large_funnels = 9
|
||||||
|
option_trees = 10
|
||||||
|
option_choices = 11
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
from worlds.factorio.Options import TechTreeLayout
|
from worlds.factorio.Options import TechTreeLayout
|
||||||
|
|
||||||
@@ -21,7 +22,9 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
|
|||||||
tech_names.sort()
|
tech_names.sort()
|
||||||
world.random.shuffle(tech_names)
|
world.random.shuffle(tech_names)
|
||||||
|
|
||||||
if layout == TechTreeLayout.option_small_diamonds:
|
if layout == TechTreeLayout.option_single:
|
||||||
|
pass
|
||||||
|
elif layout == TechTreeLayout.option_small_diamonds:
|
||||||
slice_size = 4
|
slice_size = 4
|
||||||
while len(tech_names) > slice_size:
|
while len(tech_names) > slice_size:
|
||||||
slice = tech_names[:slice_size]
|
slice = tech_names[:slice_size]
|
||||||
@@ -189,6 +192,55 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
|
|||||||
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
|
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
|
||||||
previous_slice = slice
|
previous_slice = slice
|
||||||
layer_size -= 1
|
layer_size -= 1
|
||||||
|
elif layout == TechTreeLayout.option_trees:
|
||||||
|
# 0 |
|
||||||
|
# 1 2 |
|
||||||
|
# 3 |
|
||||||
|
# 4 5 6 7 |
|
||||||
|
# 8 |
|
||||||
|
# 9 10 11 12 13 14 |
|
||||||
|
# 15 |
|
||||||
|
# 16 |
|
||||||
|
slice_size = 17
|
||||||
|
while len(tech_names) > slice_size:
|
||||||
|
slice = tech_names[:slice_size]
|
||||||
|
tech_names = tech_names[slice_size:]
|
||||||
|
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
|
||||||
|
|
||||||
|
prerequisites[slice[1]] = {slice[0]}
|
||||||
|
prerequisites[slice[2]] = {slice[0]}
|
||||||
|
|
||||||
|
prerequisites[slice[3]] = {slice[1], slice[2]}
|
||||||
|
|
||||||
|
prerequisites[slice[4]] = {slice[3]}
|
||||||
|
prerequisites[slice[5]] = {slice[3]}
|
||||||
|
prerequisites[slice[6]] = {slice[3]}
|
||||||
|
prerequisites[slice[7]] = {slice[3]}
|
||||||
|
|
||||||
|
prerequisites[slice[8]] = {slice[4], slice[5], slice[6], slice[7]}
|
||||||
|
|
||||||
|
prerequisites[slice[9]] = {slice[8]}
|
||||||
|
prerequisites[slice[10]] = {slice[8]}
|
||||||
|
prerequisites[slice[11]] = {slice[8]}
|
||||||
|
prerequisites[slice[12]] = {slice[8]}
|
||||||
|
prerequisites[slice[13]] = {slice[8]}
|
||||||
|
prerequisites[slice[14]] = {slice[8]}
|
||||||
|
|
||||||
|
prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]}
|
||||||
|
prerequisites[slice[16]] = {slice[15]}
|
||||||
|
elif layout == TechTreeLayout.option_choices:
|
||||||
|
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
|
||||||
|
current_choices = deque([tech_names[0]])
|
||||||
|
tech_names = tech_names[1:]
|
||||||
|
while len(tech_names) > 1:
|
||||||
|
source = current_choices.pop()
|
||||||
|
choices = tech_names[:2]
|
||||||
|
tech_names = tech_names[2:]
|
||||||
|
for choice in choices:
|
||||||
|
prerequisites[choice] = {source}
|
||||||
|
current_choices.extendleft(choices)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"Layout {layout} is not implemented.")
|
||||||
|
|
||||||
world.tech_tree_layout_prerequisites[player] = prerequisites
|
world.tech_tree_layout_prerequisites[player] = prerequisites
|
||||||
return prerequisites
|
return prerequisites
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
kivy>=2.0.0
|
|
||||||
factorio-rcon-py>=1.2.1
|
factorio-rcon-py>=1.2.1
|
||||||
schema>=0.7.4
|
schema>=0.7.4
|
||||||
|
|||||||
@@ -282,22 +282,22 @@ class Rom(BigStream):
|
|||||||
|
|
||||||
|
|
||||||
def compress_rom_file(input_file, output_file):
|
def compress_rom_file(input_file, output_file):
|
||||||
subcall = []
|
|
||||||
|
|
||||||
compressor_path = data_path("Compress")
|
compressor_path = data_path("Compress")
|
||||||
|
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
compressor_path += "\\Compress.exe"
|
executable_path = "Compress.exe"
|
||||||
elif platform.system() == 'Linux':
|
elif platform.system() == 'Linux':
|
||||||
if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
|
if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
|
||||||
compressor_path += "/Compress_ARM64"
|
executable_path = "Compress_ARM64"
|
||||||
else:
|
else:
|
||||||
compressor_path += "/Compress"
|
executable_path = "Compress"
|
||||||
elif platform.system() == 'Darwin':
|
elif platform.system() == 'Darwin':
|
||||||
compressor_path += "/Compress.out"
|
executable_path = "Compress.out"
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('Unsupported operating system for compression.')
|
raise RuntimeError('Unsupported operating system for compression.')
|
||||||
|
compressor_path = os.path.join(compressor_path, executable_path)
|
||||||
if not os.path.exists(compressor_path):
|
if not os.path.exists(compressor_path):
|
||||||
raise RuntimeError(f'Compressor does not exist! Please place it at {compressor_path}.')
|
raise RuntimeError(f'Compressor does not exist! Please place it at {compressor_path}.')
|
||||||
process = subprocess.call([compressor_path, input_file, output_file], **subprocess_args(include_stdout=False))
|
import logging
|
||||||
|
logging.info(subprocess.check_output([compressor_path, input_file, output_file],
|
||||||
|
**subprocess_args(include_stdout=False)))
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class StartLocation(Choice):
|
|||||||
|
|
||||||
class DeathLink(Choice):
|
class DeathLink(Choice):
|
||||||
"""When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you."""
|
"""When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you."""
|
||||||
displayname = "Death Link Survive"
|
displayname = "Death Link"
|
||||||
option_disable = 0
|
option_disable = 0
|
||||||
option_enable = 1
|
option_enable = 1
|
||||||
option_enable_survive = 3
|
option_enable_survive = 3
|
||||||
|
|||||||
@@ -348,8 +348,7 @@ class VariaRandomizer:
|
|||||||
if response.ok:
|
if response.ok:
|
||||||
PresetLoader.factory(json.loads(response.text)).load(self.player)
|
PresetLoader.factory(json.loads(response.text)).load(self.player)
|
||||||
else:
|
else:
|
||||||
print("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
|
raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
|
||||||
sys.exit(-1)
|
|
||||||
else:
|
else:
|
||||||
preset = 'default'
|
preset = 'default'
|
||||||
PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player)
|
PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player)
|
||||||
@@ -365,13 +364,11 @@ class VariaRandomizer:
|
|||||||
self.seed = args.seed
|
self.seed = args.seed
|
||||||
logger.debug("seed: {}".format(self.seed))
|
logger.debug("seed: {}".format(self.seed))
|
||||||
|
|
||||||
seed4rand = self.seed
|
|
||||||
if args.raceMagic is not None:
|
if args.raceMagic is not None:
|
||||||
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
|
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
|
||||||
print("Invalid magic")
|
print("Invalid magic")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
seed4rand = self.seed ^ args.raceMagic
|
|
||||||
# random.seed(seed4rand)
|
|
||||||
# if no max diff, set it very high
|
# if no max diff, set it very high
|
||||||
if args.maxDifficulty:
|
if args.maxDifficulty:
|
||||||
if args.maxDifficulty == 'random':
|
if args.maxDifficulty == 'random':
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ class EvermizerFlag:
|
|||||||
return self.flag if self.value != self.default else ''
|
return self.flag if self.value != self.default else ''
|
||||||
|
|
||||||
|
|
||||||
class OffOnChaosChoice(Choice):
|
class OffOnFullChoice(Choice):
|
||||||
option_off = 0
|
option_off = 0
|
||||||
option_on = 1
|
option_on = 1
|
||||||
option_chaos = 2
|
option_full = 2
|
||||||
|
alias_chaos = 2
|
||||||
alias_false = 0
|
alias_false = 0
|
||||||
alias_true = 1
|
alias_true = 1
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ class Difficulty(EvermizerFlags, Choice):
|
|||||||
option_easy = 0
|
option_easy = 0
|
||||||
option_normal = 1
|
option_normal = 1
|
||||||
option_hard = 2
|
option_hard = 2
|
||||||
option_chaos = 3 # random is reserved pre 0.2
|
option_mystery = 3 # 'random' is reserved
|
||||||
|
alias_chaos = 3
|
||||||
default = 1
|
default = 1
|
||||||
flags = ['e', 'n', 'h', 'x']
|
flags = ['e', 'n', 'h', 'x']
|
||||||
|
|
||||||
@@ -88,27 +90,27 @@ class ShorterDialogs(EvermizerFlag, Toggle):
|
|||||||
|
|
||||||
|
|
||||||
class ShortBossRush(EvermizerFlag, Toggle):
|
class ShortBossRush(EvermizerFlag, Toggle):
|
||||||
"""Start boss rush at Magmar, cut HP in half"""
|
"""Start boss rush at Metal Magmar, cut enemy HP in half"""
|
||||||
displayname = "Short Boss Rush"
|
displayname = "Short Boss Rush"
|
||||||
flag = 'f'
|
flag = 'f'
|
||||||
|
|
||||||
|
|
||||||
class Ingredienizer(EvermizerFlags, OffOnChaosChoice):
|
class Ingredienizer(EvermizerFlags, OffOnFullChoice):
|
||||||
"""Shuffles or randomizes spell ingredients"""
|
"""On Shuffles, Full randomizes spell ingredients"""
|
||||||
displayname = "Ingredienizer"
|
displayname = "Ingredienizer"
|
||||||
default = 1
|
default = 1
|
||||||
flags = ['i', '', 'I']
|
flags = ['i', '', 'I']
|
||||||
|
|
||||||
|
|
||||||
class Sniffamizer(EvermizerFlags, OffOnChaosChoice):
|
class Sniffamizer(EvermizerFlags, OffOnFullChoice):
|
||||||
"""Shuffles or randomizes drops in sniff locations"""
|
"""On Shuffles, Full randomizes drops in sniff locations"""
|
||||||
displayname = "Sniffamizer"
|
displayname = "Sniffamizer"
|
||||||
default = 1
|
default = 1
|
||||||
flags = ['s', '', 'S']
|
flags = ['s', '', 'S']
|
||||||
|
|
||||||
|
|
||||||
class Callbeadamizer(EvermizerFlags, OffOnChaosChoice):
|
class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
|
||||||
"""Shuffles call bead characters or spells"""
|
"""On Shuffles call bead characters, Full shuffles individual spells"""
|
||||||
displayname = "Callbeadamizer"
|
displayname = "Callbeadamizer"
|
||||||
default = 1
|
default = 1
|
||||||
flags = ['c', '', 'C']
|
flags = ['c', '', 'C']
|
||||||
@@ -120,8 +122,8 @@ class Musicmizer(EvermizerFlag, Toggle):
|
|||||||
flag = 'm'
|
flag = 'm'
|
||||||
|
|
||||||
|
|
||||||
class Doggomizer(EvermizerFlags, OffOnChaosChoice):
|
class Doggomizer(EvermizerFlags, OffOnFullChoice):
|
||||||
"""On shuffles dog per act, Chaos randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
|
"""On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
|
||||||
displayname = "Doggomizer"
|
displayname = "Doggomizer"
|
||||||
option_pupdunk = 3
|
option_pupdunk = 3
|
||||||
default = 0
|
default = 0
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
|
||||||
#https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
|
#https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
|
||||||
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
|
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
|
||||||
bsdiff4>=1.2.1
|
bsdiff4>=1.2.1
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ starter_melee_weapons: Tuple[str, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
starter_spells: Tuple[str, ...] = (
|
starter_spells: Tuple[str, ...] = (
|
||||||
|
'Aura Blast',
|
||||||
'Colossal Blade',
|
'Colossal Blade',
|
||||||
'Infernal Flames',
|
'Infernal Flames',
|
||||||
'Plasma Geyser',
|
'Plasma Geyser',
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class Cantoran(Toggle):
|
|||||||
"Cantoran's fight and check are available upon revisiting his room"
|
"Cantoran's fight and check are available upon revisiting his room"
|
||||||
display_name = "Cantoran"
|
display_name = "Cantoran"
|
||||||
|
|
||||||
|
class DamageRando(Toggle):
|
||||||
|
"Each orb has a high chance of having lower base damage and a low chance of having much higher base damage."
|
||||||
|
display_name = "Damage Rando"
|
||||||
|
|
||||||
# Some options that are available in the timespinner randomizer arent currently implemented
|
# Some options that are available in the timespinner randomizer arent currently implemented
|
||||||
timespinner_options: Dict[str, Toggle] = {
|
timespinner_options: Dict[str, Toggle] = {
|
||||||
"StartWithJewelryBox": StartWithJewelryBox,
|
"StartWithJewelryBox": StartWithJewelryBox,
|
||||||
@@ -64,6 +68,7 @@ timespinner_options: Dict[str, Toggle] = {
|
|||||||
#"StinkyMaw": StinkyMaw,
|
#"StinkyMaw": StinkyMaw,
|
||||||
"GyreArchives": GyreArchives,
|
"GyreArchives": GyreArchives,
|
||||||
"Cantoran": Cantoran,
|
"Cantoran": Cantoran,
|
||||||
|
"DamageRando": DamageRando,
|
||||||
"DeathLink": DeathLink,
|
"DeathLink": DeathLink,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
|
|||||||
else:
|
else:
|
||||||
gates = (*past_teleportation_gates, *present_teleportation_gates)
|
gates = (*past_teleportation_gates, *present_teleportation_gates)
|
||||||
|
|
||||||
|
if not world:
|
||||||
|
return gates[0]
|
||||||
|
|
||||||
return world.random.choice(gates)
|
return world.random.choice(gates)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Dict, Tuple, Optional, Callable
|
from typing import List, Set, Dict, Tuple, Optional, Callable
|
||||||
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
||||||
from .Options import is_option_enabled
|
from .Options import is_option_enabled
|
||||||
from .Locations import LocationData
|
from .Locations import LocationData
|
||||||
@@ -6,7 +6,7 @@ from .Locations import LocationData
|
|||||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str):
|
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str):
|
||||||
locations_per_region = get_locations_per_region(locations)
|
locations_per_region = get_locations_per_region(locations)
|
||||||
|
|
||||||
world.regions += [
|
regions = [
|
||||||
create_region(world, player, locations_per_region, location_cache, 'Menu'),
|
create_region(world, player, locations_per_region, location_cache, 'Menu'),
|
||||||
create_region(world, player, locations_per_region, location_cache, 'Tutorial'),
|
create_region(world, player, locations_per_region, location_cache, 'Tutorial'),
|
||||||
create_region(world, player, locations_per_region, location_cache, 'Lake desolation'),
|
create_region(world, player, locations_per_region, location_cache, 'Lake desolation'),
|
||||||
@@ -45,6 +45,11 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
|
create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
||||||
|
|
||||||
|
world.regions += regions
|
||||||
|
|
||||||
connectStartingRegion(world, player)
|
connectStartingRegion(world, player)
|
||||||
|
|
||||||
names: Dict[str, int] = {}
|
names: Dict[str, int] = {}
|
||||||
@@ -94,8 +99,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player))
|
connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player))
|
||||||
connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft')
|
connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft')
|
||||||
connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player))
|
connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_doublejump(world, player))
|
||||||
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_doublejump(world, player))
|
||||||
connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Refugee Camp', 'Forest')
|
connect(world, player, names, 'Refugee Camp', 'Forest')
|
||||||
connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted"))
|
connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted"))
|
||||||
@@ -114,9 +119,9 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves')
|
connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves')
|
||||||
connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)')
|
connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)')
|
||||||
connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player))
|
connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player))
|
||||||
connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player))
|
connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_doublejump(world, player))
|
||||||
connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_doublejump(world, player))
|
||||||
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player))
|
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player))
|
||||||
connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest')
|
connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest')
|
||||||
@@ -150,6 +155,16 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||||||
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
||||||
|
|
||||||
|
|
||||||
|
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):
|
||||||
|
existingRegions = set()
|
||||||
|
|
||||||
|
for region in regions:
|
||||||
|
existingRegions.add(region.name)
|
||||||
|
|
||||||
|
if (regionNames - existingRegions):
|
||||||
|
raise Exception("Tiemspinner: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions))
|
||||||
|
|
||||||
|
|
||||||
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
|
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
|
||||||
location = Location(player, location_data.name, location_data.code, region)
|
location = Location(player, location_data.name, location_data.code, region)
|
||||||
location.access_rule = location_data.rule
|
location.access_rule = location_data.rule
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Dict, List, Set, TextIO
|
from typing import Dict, List, Set, Tuple, TextIO
|
||||||
from BaseClasses import Item, MultiWorld, Location
|
from BaseClasses import Item, MultiWorld, Location
|
||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
from .LogicMixin import TimespinnerLogic
|
from .LogicMixin import TimespinnerLogic
|
||||||
@@ -24,19 +24,31 @@ class TimespinnerWorld(World):
|
|||||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||||
item_name_groups = get_item_names_per_category()
|
item_name_groups = get_item_names_per_category()
|
||||||
|
|
||||||
locked_locations: Dict[int, List[str]] = {}
|
locked_locations: List[str]
|
||||||
pyramid_keys_unlock: Dict[int, str] = {}
|
pyramid_keys_unlock: str
|
||||||
location_cache: Dict[int, List[Location]] = {}
|
location_cache: List[Location]
|
||||||
|
|
||||||
|
def __init__(self, world: MultiWorld, player: int):
|
||||||
|
super().__init__(world, player)
|
||||||
|
|
||||||
|
self.locked_locations = []
|
||||||
|
self.location_cache = []
|
||||||
|
self.pyramid_keys_unlock = get_pyramid_keys_unlock(world, player)
|
||||||
|
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
self.locked_locations[self.player] = []
|
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
|
||||||
self.location_cache[self.player] = []
|
if self.world.start_inventory[self.player].value.pop('Meyef', 0) > 0:
|
||||||
self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player)
|
self.world.StartWithMeyef[self.player].value = self.world.StartWithMeyef[self.player].option_true
|
||||||
|
if self.world.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0:
|
||||||
|
self.world.QuickSeed[self.player].value = self.world.QuickSeed[self.player].option_true
|
||||||
|
if self.world.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0:
|
||||||
|
self.world.StartWithJewelryBox[self.player].value = self.world.StartWithJewelryBox[self.player].option_true
|
||||||
|
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
create_regions(self.world, self.player, get_locations(self.world, self.player),
|
create_regions(self.world, self.player, get_locations(self.world, self.player),
|
||||||
self.location_cache[self.player], self.pyramid_keys_unlock[self.player])
|
self.location_cache, self.pyramid_keys_unlock)
|
||||||
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
@@ -44,22 +56,22 @@ class TimespinnerWorld(World):
|
|||||||
|
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player])
|
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
|
||||||
|
|
||||||
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
||||||
|
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
excluded_items = get_excluded_items_based_on_options(self.world, self.player)
|
excluded_items = get_excluded_items(self, self.world, self.player)
|
||||||
|
|
||||||
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
|
||||||
|
|
||||||
if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"):
|
if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"):
|
||||||
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations)
|
||||||
|
|
||||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||||
|
|
||||||
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool)
|
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations, self.location_cache, pool)
|
||||||
|
|
||||||
self.world.itempool += pool
|
self.world.itempool += pool
|
||||||
|
|
||||||
@@ -73,17 +85,17 @@ class TimespinnerWorld(World):
|
|||||||
slot_data["StinkyMaw"] = True
|
slot_data["StinkyMaw"] = True
|
||||||
slot_data["ProgressiveVerticalMovement"] = False
|
slot_data["ProgressiveVerticalMovement"] = False
|
||||||
slot_data["ProgressiveKeycards"] = False
|
slot_data["ProgressiveKeycards"] = False
|
||||||
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
|
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock
|
||||||
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player])
|
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache)
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
|
|
||||||
def write_spoiler_header(self, spoiler_handle: TextIO):
|
def write_spoiler_header(self, spoiler_handle: TextIO):
|
||||||
spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock[self.player]))
|
spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock))
|
||||||
|
|
||||||
|
|
||||||
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]:
|
|
||||||
|
def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]:
|
||||||
excluded_items: Set[str] = set()
|
excluded_items: Set[str] = set()
|
||||||
|
|
||||||
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
||||||
@@ -92,25 +104,47 @@ def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[s
|
|||||||
excluded_items.add('Meyef')
|
excluded_items.add('Meyef')
|
||||||
if is_option_enabled(world, player, "QuickSeed"):
|
if is_option_enabled(world, player, "QuickSeed"):
|
||||||
excluded_items.add('Talaria Attachment')
|
excluded_items.add('Talaria Attachment')
|
||||||
|
|
||||||
|
for item in world.precollected_items[player]:
|
||||||
|
if item.name not in self.item_name_groups['UseItem']:
|
||||||
|
excluded_items.add(item.name)
|
||||||
|
|
||||||
return excluded_items
|
return excluded_items
|
||||||
|
|
||||||
|
|
||||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||||
melee_weapon = world.random.choice(starter_melee_weapons)
|
non_local_items = world.non_local_items[player].value
|
||||||
spell = world.random.choice(starter_spells)
|
|
||||||
|
|
||||||
excluded_items.add(melee_weapon)
|
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items)
|
||||||
excluded_items.add(spell)
|
if not local_starter_melee_weapons:
|
||||||
|
if 'Plasma Orb' in non_local_items:
|
||||||
|
raise Exception("Atleast one melee orb must be local")
|
||||||
|
else:
|
||||||
|
local_starter_melee_weapons = ('Plasma Orb',)
|
||||||
|
|
||||||
melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon)
|
local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items)
|
||||||
spell_item = create_item_with_correct_settings(world, player, spell)
|
if not local_starter_spells:
|
||||||
|
if 'Lightwall' in non_local_items:
|
||||||
|
raise Exception("Atleast one spell must be local")
|
||||||
|
else:
|
||||||
|
local_starter_spells = ('Lightwall',)
|
||||||
|
|
||||||
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
|
assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 1', local_starter_melee_weapons)
|
||||||
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
|
assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 2', local_starter_spells)
|
||||||
|
|
||||||
locked_locations.append('Yo Momma 1')
|
|
||||||
locked_locations.append('Yo Momma 2')
|
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
|
||||||
|
location: str, item_list: Tuple[str, ...]):
|
||||||
|
|
||||||
|
item_name = world.random.choice(item_list)
|
||||||
|
|
||||||
|
excluded_items.add(item_name)
|
||||||
|
|
||||||
|
item = create_item_with_correct_settings(world, player, item_name)
|
||||||
|
|
||||||
|
world.get_location(location, player).place_locked_item(item)
|
||||||
|
|
||||||
|
locked_locations.append(location)
|
||||||
|
|
||||||
|
|
||||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||||
@@ -133,8 +167,20 @@ def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locat
|
|||||||
|
|
||||||
|
|
||||||
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||||
progression_item = world.random.choice(starter_progression_items)
|
for item in world.precollected_items[player]:
|
||||||
location = world.random.choice(starter_progression_locations)
|
if item.name in starter_progression_items:
|
||||||
|
return
|
||||||
|
|
||||||
|
local_starter_progression_items = tuple(
|
||||||
|
item for item in starter_progression_items if item not in world.non_local_items[player].value)
|
||||||
|
non_excluded_starter_progression_locations = tuple(
|
||||||
|
location for location in starter_progression_locations if location not in world.exclude_locations[player].value)
|
||||||
|
|
||||||
|
if not local_starter_progression_items or not non_excluded_starter_progression_locations:
|
||||||
|
return
|
||||||
|
|
||||||
|
progression_item = world.random.choice(local_starter_progression_items)
|
||||||
|
location = world.random.choice(non_excluded_starter_progression_locations)
|
||||||
|
|
||||||
excluded_items.add(progression_item)
|
excluded_items.add(progression_item)
|
||||||
locked_locations.append(location)
|
locked_locations.append(location)
|
||||||
|
|||||||
Reference in New Issue
Block a user