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 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):
@@ -102,14 +102,15 @@ def main():
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
args.music = not args.disablemusic
if args.update_sprites:
run_sprite_update()
sys.exit()
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
if args.update_sprites:
run_sprite_update()
sys.exit()
if not os.path.isfile(args.rom):
adjustGUI()
else:
@@ -118,7 +119,6 @@ def main():
sys.exit(1)
args, path = adjust(args=args)
from Utils import persistent_store
if isinstance(args.sprite, Sprite):
args.sprite = args.sprite.name
persistent_store("adjuster", "last_settings_3", args)
@@ -224,7 +224,6 @@ def adjustGUI():
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
from Utils import persistent_store
if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs)
@@ -241,12 +240,16 @@ def adjustGUI():
def run_sprite_update():
import threading
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
try:
top = Tk()
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():
top.update()
print("Done updating sprites")
task.do_events()
logging.info("Done updating sprites")
def update_sprites(task, on_finish=None):
@@ -254,7 +257,7 @@ def update_sprites(task, on_finish=None):
successful = True
sprite_dir = local_path("data", "sprites", "alttpr")
os.makedirs(sprite_dir, exist_ok=True)
ctx = get_cert_none_ssl_context()
def finished():
task.close_window()
if on_finish:
@@ -262,7 +265,7 @@ def update_sprites(task, on_finish=None):
try:
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"))
except Exception as 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):
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)
def rem(sprite):
@@ -400,12 +403,39 @@ class BackgroundTaskProgress(BackgroundTask):
def update_status(self, text):
self.queue_event(lambda: self.label_var.set(text))
def do_events(self):
self.parent.update()
# only call this in an event callback
def close_window(self):
self.stop()
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):
romFrame = Frame(parent)
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
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):
@@ -35,12 +35,10 @@ def prompt_yes_no(prompt):
def find_ap_randomizer_jar(forge_dir):
mods_dir = os.path.join(forge_dir, 'mods')
if os.path.isdir(mods_dir):
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
for entry in os.scandir(mods_dir):
match = ap_mod_re.match(entry.name)
if match:
logging.info(f"Found AP randomizer mod: {match.group()}")
return match.group()
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
logging.info(f"Found AP randomizer mod: {entry.name}")
return entry.name
return None
else:
os.mkdir(mods_dir)

View File

@@ -13,6 +13,7 @@ Currently, the following games are supported:
* Timespinner
* Super Metroid
* Secret of Evermore
* Final Fantasy
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

View File

@@ -1048,6 +1048,7 @@ async def game_watcher(ctx: Context):
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
async def run_game(romfile):
auto_start = Utils.get_options()["lttp_options"].get("rom_start", True)
if auto_start is True:

View File

@@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.2.1"
__version__ = "0.2.2"
version_tuple = tuplize_version(__version__)
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 WebHostLib.models import db
from WebHostLib.autolauncher import autohost
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
@@ -45,6 +45,8 @@ if __name__ == "__main__":
create_options_files()
if app.config["SELFLAUNCH"]:
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["DEBUG"]:
autohost(app.config)

View File

@@ -22,9 +22,10 @@ Pony(app)
app.jinja_env.filters['any'] = any
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["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["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@@ -166,8 +167,9 @@ def host_room(room: UUID):
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
Command(room=room, commandtext=cmd)
commit()
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
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():
try:
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,
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()
while 1:
time.sleep(0.50)
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)
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:
pass
logging.info("Autogen reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running).start()
threading.Thread(target=keep_running, name="AP_Autogen").start()
multiworlds = {}

View File

@@ -10,6 +10,7 @@ def update_sprites_lttp():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
@@ -19,11 +20,15 @@ def update_sprites_lttp():
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
try:
top = Tk()
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():
top.update()
task.do_events()
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
[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?
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
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
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?
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.
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
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.

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,
**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]]],
inventory: Counter, team: int, player: int, playerName: str,
@@ -887,5 +987,6 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Minecraft": __renderMinecraftTracker,
"Ocarina of Time": __renderOoTTracker,
"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_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.core.window import Window
from kivy.core.clipboard import Clipboard
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.properties import BooleanProperty, ObjectProperty
from kivy.uix.button import Button
@@ -431,6 +436,4 @@ class KivyJSONtoTextParser(JSONtoTextParser):
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"))

View File

@@ -2,6 +2,6 @@ colorama>=0.4.4
websockets>=10.1
PyYAML>=6.0
fuzzywuzzy>=0.18.0
appdirs>=1.4.4
jinja2>=3.0.3
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)
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:
installfile(Path(data))
@@ -155,6 +155,7 @@ for worldname, worldtype in AutoWorldRegister.world_types.items():
file_name = worldname+".yaml"
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
buildfolder / "Players" / "Templates" / file_name)
shutil.copyfile("meta.yaml", buildfolder / "Players" / "Templates" / "meta.yaml")
try:
from maseya import z3pr

View File

@@ -2287,7 +2287,7 @@ def write_strings(rom, world, player):
if hint_count:
locations = world.find_items_in_locations(set(items_to_hint), player)
local_random.shuffle(locations)
for x in range(hint_count):
for x in range(min(hint_count, len(locations))):
this_location = locations.pop()
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
tt[hint_locations.pop(0)] = this_hint

View File

@@ -94,6 +94,8 @@ class TechTreeLayout(Choice):
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_trees = 10
option_choices = 11
default = 0

View File

@@ -1,4 +1,5 @@
from typing import Dict, List, Set
from collections import deque
from worlds.factorio.Options import TechTreeLayout
@@ -21,7 +22,9 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
tech_names.sort()
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
while len(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])
previous_slice = slice
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
return prerequisites

View File

@@ -1,3 +1,2 @@
kivy>=2.0.0
factorio-rcon-py>=1.2.1
schema>=0.7.4

View File

@@ -282,22 +282,22 @@ class Rom(BigStream):
def compress_rom_file(input_file, output_file):
subcall = []
compressor_path = data_path("Compress")
if platform.system() == 'Windows':
compressor_path += "\\Compress.exe"
executable_path = "Compress.exe"
elif platform.system() == 'Linux':
if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
compressor_path += "/Compress_ARM64"
executable_path = "Compress_ARM64"
else:
compressor_path += "/Compress"
executable_path = "Compress"
elif platform.system() == 'Darwin':
compressor_path += "/Compress.out"
executable_path = "Compress.out"
else:
raise RuntimeError('Unsupported operating system for compression.')
compressor_path = os.path.join(compressor_path, executable_path)
if not os.path.exists(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):
"""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_enable = 1
option_enable_survive = 3

View File

@@ -348,8 +348,7 @@ class VariaRandomizer:
if response.ok:
PresetLoader.factory(json.loads(response.text)).load(self.player)
else:
print("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
sys.exit(-1)
raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
else:
preset = 'default'
PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player)
@@ -365,13 +364,11 @@ class VariaRandomizer:
self.seed = args.seed
logger.debug("seed: {}".format(self.seed))
seed4rand = self.seed
if args.raceMagic is not None:
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
print("Invalid magic")
sys.exit(-1)
seed4rand = self.seed ^ args.raceMagic
# random.seed(seed4rand)
# if no max diff, set it very high
if args.maxDifficulty:
if args.maxDifficulty == 'random':

View File

@@ -16,10 +16,11 @@ class EvermizerFlag:
return self.flag if self.value != self.default else ''
class OffOnChaosChoice(Choice):
class OffOnFullChoice(Choice):
option_off = 0
option_on = 1
option_chaos = 2
option_full = 2
alias_chaos = 2
alias_false = 0
alias_true = 1
@@ -30,7 +31,8 @@ class Difficulty(EvermizerFlags, Choice):
option_easy = 0
option_normal = 1
option_hard = 2
option_chaos = 3 # random is reserved pre 0.2
option_mystery = 3 # 'random' is reserved
alias_chaos = 3
default = 1
flags = ['e', 'n', 'h', 'x']
@@ -88,27 +90,27 @@ class ShorterDialogs(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"
flag = 'f'
class Ingredienizer(EvermizerFlags, OffOnChaosChoice):
"""Shuffles or randomizes spell ingredients"""
class Ingredienizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles, Full randomizes spell ingredients"""
displayname = "Ingredienizer"
default = 1
flags = ['i', '', 'I']
class Sniffamizer(EvermizerFlags, OffOnChaosChoice):
"""Shuffles or randomizes drops in sniff locations"""
class Sniffamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles, Full randomizes drops in sniff locations"""
displayname = "Sniffamizer"
default = 1
flags = ['s', '', 'S']
class Callbeadamizer(EvermizerFlags, OffOnChaosChoice):
"""Shuffles call bead characters or spells"""
class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles call bead characters, Full shuffles individual spells"""
displayname = "Callbeadamizer"
default = 1
flags = ['c', '', 'C']
@@ -120,8 +122,8 @@ class Musicmizer(EvermizerFlag, Toggle):
flag = 'm'
class Doggomizer(EvermizerFlags, OffOnChaosChoice):
"""On shuffles dog per act, Chaos randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
class Doggomizer(EvermizerFlags, OffOnFullChoice):
"""On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
displayname = "Doggomizer"
option_pupdunk = 3
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.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.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.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.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.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.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.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.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.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.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.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.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-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-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-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-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-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-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-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-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-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-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.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.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.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

View File

@@ -213,6 +213,7 @@ starter_melee_weapons: Tuple[str, ...] = (
)
starter_spells: Tuple[str, ...] = (
'Aura Blast',
'Colossal Blade',
'Infernal Flames',
'Plasma Geyser',

View File

@@ -50,6 +50,10 @@ class Cantoran(Toggle):
"Cantoran's fight and check are available upon revisiting his room"
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
timespinner_options: Dict[str, Toggle] = {
"StartWithJewelryBox": StartWithJewelryBox,
@@ -64,6 +68,7 @@ timespinner_options: Dict[str, Toggle] = {
#"StinkyMaw": StinkyMaw,
"GyreArchives": GyreArchives,
"Cantoran": Cantoran,
"DamageRando": DamageRando,
"DeathLink": DeathLink,
}

View File

@@ -27,4 +27,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
else:
gates = (*past_teleportation_gates, *present_teleportation_gates)
if not world:
return gates[0]
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 .Options import is_option_enabled
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):
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, 'Tutorial'),
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')
]
if __debug__:
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
world.regions += regions
connectStartingRegion(world, player)
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', '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)', '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 (Xarion)', 'Sealed Caves (upper)', lambda state: 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_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, 'Refugee Camp', 'Forest')
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', '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)', '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 (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)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
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")
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:
location = Location(player, location_data.name, location_data.code, region)
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 ..AutoWorld import World
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)}
item_name_groups = get_item_names_per_category()
locked_locations: Dict[int, List[str]] = {}
pyramid_keys_unlock: Dict[int, str] = {}
location_cache: Dict[int, List[Location]] = {}
locked_locations: List[str]
pyramid_keys_unlock: str
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):
self.locked_locations[self.player] = []
self.location_cache[self.player] = []
self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player)
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
if self.world.start_inventory[self.player].value.pop('Meyef', 0) > 0:
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):
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:
@@ -44,22 +56,22 @@ class TimespinnerWorld(World):
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)
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"):
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)
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
@@ -73,17 +85,17 @@ class TimespinnerWorld(World):
slot_data["StinkyMaw"] = True
slot_data["ProgressiveVerticalMovement"] = False
slot_data["ProgressiveKeycards"] = False
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player])
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache)
return slot_data
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()
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')
if is_option_enabled(world, player, "QuickSeed"):
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
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
melee_weapon = world.random.choice(starter_melee_weapons)
spell = world.random.choice(starter_spells)
non_local_items = world.non_local_items[player].value
excluded_items.add(melee_weapon)
excluded_items.add(spell)
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items)
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)
spell_item = create_item_with_correct_settings(world, player, spell)
local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items)
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)
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 1', local_starter_melee_weapons)
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]:
@@ -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]):
progression_item = world.random.choice(starter_progression_items)
location = world.random.choice(starter_progression_locations)
for item in world.precollected_items[player]:
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)
locked_locations.append(location)