Compare commits

...

34 Commits
0.2.1 ... 0.2.2

Author SHA1 Message Date
Fabian Dill
97d6e80556 Bump 2021-12-21 15:31:04 +01:00
Fabian Dill
d5abadc6d0 Requirements: remove no longer used appdirs and move kivy to core 2021-12-20 23:10:04 +01:00
Jarno
d08d716966 [Timespinner] Added orb damage rando flag 2021-12-20 14:40:01 +00:00
espeon65536
3ee4be2e33 Minecraft client: more general search for mod name 2021-12-19 19:15:09 +00:00
black-sliver
9172cc4925 SoE: Update to pyevermizer v0.40.0
see https://github.com/black-sliver/pyevermizer/releases/tag/v0.40.0
2021-12-19 15:22:19 +00:00
black-sliver
7f03a86dee SoE: Rename 'chaos' to 'full' in options
* was changed upstream
* also update tooltips to be a bit more helpful
2021-12-19 15:22:19 +00:00
black-sliver
1603bab1da SoE: Rename difficulty 'Chaos' to 'Mystery' 2021-12-19 15:22:19 +00:00
black-sliver
70aae514be SoE: fix macos wheel urls 2021-12-19 15:22:19 +00:00
black-sliver
5fa1185d6d SoE: make doc point to upstream guide.md 2021-12-19 15:22:19 +00:00
Fabian Dill
3a2a584ad3 Factorio: fix singles layout not generating correctly. 2021-12-18 13:05:43 +01:00
Fabian Dill
c42f53d64f Factorio: add some more tech tree shapes 2021-12-18 13:01:30 +01:00
Jarno Westhof
450e0eacf4 TS: Relaxed entry logic for lower caves 2021-12-17 19:50:38 +00:00
Fabian Dill
aa40e811f1 LttPAdjuster: ignore alttpr cert 2021-12-17 19:17:41 +01:00
CaitSith2
af96f71190 Fix bug where there is less locations than hint count. 2021-12-16 15:34:18 -08:00
Jarno Westhof
9e4cb6ee33 TS: Fixed review comments 2021-12-14 16:04:50 +00:00
Jarno Westhof
5d0748983b TS: removed todo list :D 2021-12-14 16:04:50 +00:00
Jarno Westhof
c4981e4b91 TS: Fixed unit test 2021-12-14 16:04:50 +00:00
Jarno Westhof
3f36c436ad TS: putting items as non local will correctly be handled by your starting orbs and your first progression item
excluding locations now correctly works for your first progression item in an non inverted seed
Aura blast can now be your starting spell
2021-12-14 16:04:50 +00:00
Jarno Westhof
db456cbcf1 TS: no longer reward a progression item if you already have one in your starting inventory 2021-12-14 16:04:50 +00:00
Jarno Westhof
c0b8384319 TS: putting non consumable items in starting inventory will now remove them from the pool so a duplicate wont drop 2021-12-14 16:04:50 +00:00
Jarno Westhof
13036539b7 TS: Starting with Jewelrybox, Talaria or Meyef in your starting inventory will now set the corresponding flag 2021-12-14 16:04:50 +00:00
Jarno Westhof
5a2e477dba Added sanity check to see if all locations can be assigned to regions 2021-12-14 16:04:50 +00:00
TauAkiou
f003c7130f [WebHost] Add Super Metroid support to Web Tracker (#153)
* [WebHost]: Added Super Metroid tracker, based on TimeSpinner & OOT
2021-12-14 17:04:24 +01:00
CaitSith2
0558351a12 Allow update_sprites to work on strict text only systems 2021-12-13 20:24:54 +01:00
Fabian Dill
3f20bdaaa2 WebHost: split autolaunch and autogen services 2021-12-13 05:48:33 +01:00
Fabian Dill
3bf367d630 WebHost: don't bother queuing empty commands 2021-12-13 01:38:07 +01:00
alwaysintreble
706fc19ab4 tutorials: place a missing / oops 2021-12-11 17:04:07 +00:00
espeon65536
4fe024041d Minecraft client: update Forge to 1.17.1-37.1.1
This fixes the critical security issue recently found in Minecraft.
2021-12-10 19:43:57 +00:00
Fabian Dill
7afbf8b45b OoTAdjuster: check on subprocess compressor 2021-12-10 09:53:50 +01:00
Fabian Dill
e1fc44f4e0 Clients: compatibility change for old Intel graphics. 2021-12-10 09:29:59 +01:00
jtoyoda
21fbb545e8 Adding in missing comas in ff1 game info 2021-12-08 14:23:01 +00:00
jtoyoda
6cd08ea8dc Updating ff1 gameinfo 2021-12-08 14:23:01 +00:00
Fabian Dill
85efee1432 SM: raise Exception instead of sys.exit for custom presets 2021-12-08 09:27:58 +01:00
Hussein Farran
ba9974fe2a Update README.md 2021-12-04 23:07:35 +00:00
33 changed files with 645 additions and 129 deletions

View File

@@ -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: ')

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = {}

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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

View 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;
});
}
});

View File

@@ -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.

View 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;
}

View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;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>

View File

@@ -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
} }

View File

@@ -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"))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)))

View File

@@ -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

View File

@@ -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':

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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,
} }

View File

@@ -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)

View File

@@ -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

View File

@@ -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"):
@@ -93,24 +105,46 @@ def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[s
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)