Undertale for AP (#439)
Randomizes the items, and adds a new item to the pool, "Plot" which lets you go further and further in the game the more you have. Developers: WirTheAvali (Preferred name for professional use, mewlif)
This commit is contained in:
parent
71bfb6babd
commit
553fe0be19
|
@ -28,6 +28,7 @@
|
||||||
*.apsave
|
*.apsave
|
||||||
*.BIN
|
*.BIN
|
||||||
|
|
||||||
|
setups
|
||||||
build
|
build
|
||||||
bundle/components.wxs
|
bundle/components.wxs
|
||||||
dist
|
dist
|
||||||
|
@ -176,6 +177,9 @@ minecraft_versions.json
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
#undertale stuff
|
||||||
|
/Undertale/
|
||||||
|
|
||||||
# OS General Files
|
# OS General Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
|
|
|
@ -45,6 +45,7 @@ Currently, the following games are supported:
|
||||||
* Adventure
|
* Adventure
|
||||||
* DLC Quest
|
* DLC Quest
|
||||||
* Noita
|
* Noita
|
||||||
|
* Undertale
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,498 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import typing
|
||||||
|
import bsdiff4
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
|
||||||
|
from NetUtils import NetworkItem, ClientStatus
|
||||||
|
from worlds import undertale
|
||||||
|
from MultiServer import mark_raw
|
||||||
|
from CommonClient import CommonContext, server_loop, \
|
||||||
|
gui_enabled, ClientCommandProcessor, get_base_parser
|
||||||
|
from Utils import async_start
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||||
|
def __init__(self, ctx):
|
||||||
|
super().__init__(ctx)
|
||||||
|
|
||||||
|
def _cmd_resync(self):
|
||||||
|
"""Manually trigger a resync."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.output(f"Syncing items.")
|
||||||
|
self.ctx.syncing = True
|
||||||
|
|
||||||
|
def _cmd_patch(self):
|
||||||
|
"""Patch the game."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||||
|
self.ctx.patch_game()
|
||||||
|
self.output("Patched.")
|
||||||
|
|
||||||
|
@mark_raw
|
||||||
|
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||||
|
"""Patch the game automatically."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||||
|
tempInstall = steaminstall
|
||||||
|
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
|
tempInstall = None
|
||||||
|
if tempInstall is None:
|
||||||
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||||
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
elif not os.path.exists(tempInstall):
|
||||||
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||||
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
|
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
||||||
|
" command. \"/auto_patch (Steam directory)\".")
|
||||||
|
else:
|
||||||
|
for file_name in os.listdir(tempInstall):
|
||||||
|
if file_name != "steam_api.dll":
|
||||||
|
shutil.copy(tempInstall+"\\"+file_name,
|
||||||
|
os.getcwd() + "\\Undertale\\" + file_name)
|
||||||
|
self.ctx.patch_game()
|
||||||
|
self.output("Patching successful!")
|
||||||
|
|
||||||
|
def _cmd_online(self):
|
||||||
|
"""Makes you no longer able to see other Undertale players."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.ctx.update_online_mode(not ("Online" in self.ctx.tags))
|
||||||
|
if "Online" in self.ctx.tags:
|
||||||
|
self.output(f"Now online.")
|
||||||
|
else:
|
||||||
|
self.output(f"Now offline.")
|
||||||
|
|
||||||
|
def _cmd_deathlink(self):
|
||||||
|
"""Toggles deathlink"""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.ctx.deathlink_status = not self.ctx.deathlink_status
|
||||||
|
if self.ctx.deathlink_status:
|
||||||
|
self.output(f"Deathlink enabled.")
|
||||||
|
else:
|
||||||
|
self.output(f"Deathlink disabled.")
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleContext(CommonContext):
|
||||||
|
tags = {"AP", "Online"}
|
||||||
|
game = "Undertale"
|
||||||
|
command_processor = UndertaleCommandProcessor
|
||||||
|
items_handling = 0b111
|
||||||
|
route = None
|
||||||
|
pieces_needed = None
|
||||||
|
completed_routes = None
|
||||||
|
completed_count = 0
|
||||||
|
save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
self.pieces_needed = 0
|
||||||
|
self.game = "Undertale"
|
||||||
|
self.got_deathlink = False
|
||||||
|
self.syncing = False
|
||||||
|
self.deathlink_status = False
|
||||||
|
self.tem_armor = False
|
||||||
|
self.completed_count = 0
|
||||||
|
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||||
|
|
||||||
|
def patch_game(self):
|
||||||
|
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
|
||||||
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
|
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
|
||||||
|
f.write(patchedFile)
|
||||||
|
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
|
||||||
|
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
|
||||||
|
"Which Character.txt"), "w") as f:
|
||||||
|
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
||||||
|
"line other than this one.\n", "frisk"])
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super().server_auth(password_requested)
|
||||||
|
await self.get_username()
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
def clear_undertale_files(self):
|
||||||
|
path = self.save_game_folder
|
||||||
|
self.finished_game = False
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if "check.spot" == file or "scout" == file:
|
||||||
|
os.remove(os.path.join(root, file))
|
||||||
|
elif file.endswith((".item", ".victory", ".route", ".playerspot", ".mad",
|
||||||
|
".youDied", ".LV", ".mine", ".flag", ".hint")):
|
||||||
|
os.remove(os.path.join(root, file))
|
||||||
|
|
||||||
|
async def connect(self, address: typing.Optional[str] = None):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().connect(address)
|
||||||
|
|
||||||
|
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().disconnect(allow_autoreconnect)
|
||||||
|
|
||||||
|
async def connection_closed(self):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().connection_closed()
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().shutdown()
|
||||||
|
|
||||||
|
def update_online_mode(self, online):
|
||||||
|
old_tags = self.tags.copy()
|
||||||
|
if online:
|
||||||
|
self.tags.add("Online")
|
||||||
|
else:
|
||||||
|
self.tags -= {"Online"}
|
||||||
|
if old_tags != self.tags and self.server and not self.server.socket.closed:
|
||||||
|
async_start(self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]))
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd == "Connected":
|
||||||
|
self.game = self.slot_info[self.slot].game
|
||||||
|
async_start(process_undertale_cmd(self, cmd, args))
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
from kvui import GameManager
|
||||||
|
|
||||||
|
class UTManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
base_title = "Archipelago Undertale Client"
|
||||||
|
|
||||||
|
self.ui = UTManager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
def on_deathlink(self, data: typing.Dict[str, typing.Any]):
|
||||||
|
self.got_deathlink = True
|
||||||
|
super().on_deathlink(data)
|
||||||
|
|
||||||
|
|
||||||
|
def to_room_name(place_name: str):
|
||||||
|
if place_name == "Old Home Exit":
|
||||||
|
return "room_ruinsexit"
|
||||||
|
elif place_name == "Snowdin Forest":
|
||||||
|
return "room_tundra1"
|
||||||
|
elif place_name == "Snowdin Town Exit":
|
||||||
|
return "room_fogroom"
|
||||||
|
elif place_name == "Waterfall":
|
||||||
|
return "room_water1"
|
||||||
|
elif place_name == "Waterfall Exit":
|
||||||
|
return "room_fire2"
|
||||||
|
elif place_name == "Hotland":
|
||||||
|
return "room_fire_prelab"
|
||||||
|
elif place_name == "Hotland Exit":
|
||||||
|
return "room_fire_precore"
|
||||||
|
elif place_name == "Core":
|
||||||
|
return "room_fire_core1"
|
||||||
|
|
||||||
|
|
||||||
|
async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||||
|
if cmd == "Connected":
|
||||||
|
if not os.path.exists(ctx.save_game_folder):
|
||||||
|
os.mkdir(ctx.save_game_folder)
|
||||||
|
ctx.route = args["slot_data"]["route"]
|
||||||
|
ctx.pieces_needed = args["slot_data"]["key_pieces"]
|
||||||
|
ctx.tem_armor = args["slot_data"]["temy_armor_include"]
|
||||||
|
|
||||||
|
await ctx.send_msgs([{"cmd": "Get", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||||
|
str(ctx.slot)+" RoutesDone pacifist",
|
||||||
|
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||||
|
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||||
|
str(ctx.slot)+" RoutesDone pacifist",
|
||||||
|
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||||
|
if args["slot_data"]["only_flakes"]:
|
||||||
|
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
||||||
|
f.close()
|
||||||
|
if not args["slot_data"]["key_hunt"]:
|
||||||
|
ctx.pieces_needed = 0
|
||||||
|
if args["slot_data"]["rando_love"]:
|
||||||
|
filename = f"LOVErando.LV"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.close()
|
||||||
|
if args["slot_data"]["rando_stats"]:
|
||||||
|
filename = f"STATrando.LV"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.close()
|
||||||
|
filename = f"{ctx.route}.route"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.close()
|
||||||
|
filename = f"check.spot"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
||||||
|
for ss in ctx.checked_locations:
|
||||||
|
f.write(str(ss-12000)+"\n")
|
||||||
|
f.close()
|
||||||
|
elif cmd == "LocationInfo":
|
||||||
|
for l in args["locations"]:
|
||||||
|
locationid = l.location
|
||||||
|
filename = f"{str(locationid-12000)}.hint"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
toDraw = ""
|
||||||
|
for i in range(20):
|
||||||
|
if i < len(str(ctx.item_names[l.item])):
|
||||||
|
toDraw += str(ctx.item_names[l.item])[i]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
f.write(toDraw)
|
||||||
|
f.close()
|
||||||
|
elif cmd == "Retrieved":
|
||||||
|
if str(ctx.slot)+" RoutesDone neutral" in args["keys"]:
|
||||||
|
if args["keys"][str(ctx.slot)+" RoutesDone neutral"] is not None:
|
||||||
|
ctx.completed_routes["neutral"] = args["keys"][str(ctx.slot)+" RoutesDone neutral"]
|
||||||
|
if str(ctx.slot)+" RoutesDone genocide" in args["keys"]:
|
||||||
|
if args["keys"][str(ctx.slot)+" RoutesDone genocide"] is not None:
|
||||||
|
ctx.completed_routes["genocide"] = args["keys"][str(ctx.slot)+" RoutesDone genocide"]
|
||||||
|
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
||||||
|
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
||||||
|
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
||||||
|
elif cmd == "SetReply":
|
||||||
|
if args["value"] is not None:
|
||||||
|
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
||||||
|
ctx.completed_routes["pacifist"] = args["value"]
|
||||||
|
elif str(ctx.slot)+" RoutesDone genocide" == args["key"]:
|
||||||
|
ctx.completed_routes["genocide"] = args["value"]
|
||||||
|
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
||||||
|
ctx.completed_routes["neutral"] = args["value"]
|
||||||
|
elif cmd == "ReceivedItems":
|
||||||
|
start_index = args["index"]
|
||||||
|
|
||||||
|
if start_index == 0:
|
||||||
|
ctx.items_received = []
|
||||||
|
elif start_index != len(ctx.items_received):
|
||||||
|
sync_msg = [{"cmd": "Sync"}]
|
||||||
|
if ctx.locations_checked:
|
||||||
|
sync_msg.append({"cmd": "LocationChecks",
|
||||||
|
"locations": list(ctx.locations_checked)})
|
||||||
|
await ctx.send_msgs(sync_msg)
|
||||||
|
if start_index == len(ctx.items_received):
|
||||||
|
counter = -1
|
||||||
|
placedWeapon = 0
|
||||||
|
placedArmor = 0
|
||||||
|
for item in args["items"]:
|
||||||
|
id = NetworkItem(*item).location
|
||||||
|
while NetworkItem(*item).location < 0 and \
|
||||||
|
counter <= id:
|
||||||
|
id -= 1
|
||||||
|
if NetworkItem(*item).location < 0:
|
||||||
|
counter -= 1
|
||||||
|
filename = f"{str(id)}PLR{str(NetworkItem(*item).player)}.item"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
if NetworkItem(*item).item == 77701:
|
||||||
|
if placedWeapon == 0:
|
||||||
|
f.write(str(77013-11000))
|
||||||
|
elif placedWeapon == 1:
|
||||||
|
f.write(str(77014-11000))
|
||||||
|
elif placedWeapon == 2:
|
||||||
|
f.write(str(77025-11000))
|
||||||
|
elif placedWeapon == 3:
|
||||||
|
f.write(str(77045-11000))
|
||||||
|
elif placedWeapon == 4:
|
||||||
|
f.write(str(77049-11000))
|
||||||
|
elif placedWeapon == 5:
|
||||||
|
f.write(str(77047-11000))
|
||||||
|
elif placedWeapon == 6:
|
||||||
|
if str(ctx.route) == "genocide" or str(ctx.route) == "all_routes":
|
||||||
|
f.write(str(77052-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77051-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77003-11000))
|
||||||
|
placedWeapon += 1
|
||||||
|
elif NetworkItem(*item).item == 77702:
|
||||||
|
if placedArmor == 0:
|
||||||
|
f.write(str(77012-11000))
|
||||||
|
elif placedArmor == 1:
|
||||||
|
f.write(str(77015-11000))
|
||||||
|
elif placedArmor == 2:
|
||||||
|
f.write(str(77024-11000))
|
||||||
|
elif placedArmor == 3:
|
||||||
|
f.write(str(77044-11000))
|
||||||
|
elif placedArmor == 4:
|
||||||
|
f.write(str(77048-11000))
|
||||||
|
elif placedArmor == 5:
|
||||||
|
if str(ctx.route) == "genocide":
|
||||||
|
f.write(str(77053-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77046-11000))
|
||||||
|
elif placedArmor == 6 and ((not str(ctx.route) == "genocide") or ctx.tem_armor):
|
||||||
|
if str(ctx.route) == "all_routes":
|
||||||
|
f.write(str(77053-11000))
|
||||||
|
elif str(ctx.route) == "genocide":
|
||||||
|
f.write(str(77064-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77050-11000))
|
||||||
|
elif placedArmor == 7 and ctx.tem_armor and not str(ctx.route) == "genocide":
|
||||||
|
f.write(str(77064-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77004-11000))
|
||||||
|
placedArmor += 1
|
||||||
|
else:
|
||||||
|
f.write(str(NetworkItem(*item).item-11000))
|
||||||
|
f.close()
|
||||||
|
ctx.items_received.append(NetworkItem(*item))
|
||||||
|
if [item.item for item in ctx.items_received].count(77000) >= ctx.pieces_needed > 0:
|
||||||
|
filename = f"{str(-99999)}PLR{str(0)}.item"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.write(str(77787 - 11000))
|
||||||
|
f.close()
|
||||||
|
filename = f"{str(-99998)}PLR{str(0)}.item"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.write(str(77789 - 11000))
|
||||||
|
f.close()
|
||||||
|
ctx.watcher_event.set()
|
||||||
|
|
||||||
|
elif cmd == "RoomUpdate":
|
||||||
|
if "checked_locations" in args:
|
||||||
|
filename = f"check.spot"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
||||||
|
for ss in ctx.checked_locations:
|
||||||
|
f.write(str(ss-12000)+"\n")
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
elif cmd == "Bounced":
|
||||||
|
tags = args.get("tags", [])
|
||||||
|
if "Online" in tags:
|
||||||
|
data = args.get("worlds/undertale/data", {})
|
||||||
|
if data["player"] != ctx.slot and data["player"] is not None:
|
||||||
|
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.write(str(data["x"]) + str(data["y"]) + str(data["room"]) + str(
|
||||||
|
data["spr"]) + str(data["frm"]))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def multi_watcher(ctx: UndertaleContext):
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
path = ctx.save_game_folder
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if "spots.mine" in file and "Online" in ctx.tags:
|
||||||
|
with open(root + "/" + file, "r") as mine:
|
||||||
|
this_x = mine.readline()
|
||||||
|
this_y = mine.readline()
|
||||||
|
this_room = mine.readline()
|
||||||
|
this_sprite = mine.readline()
|
||||||
|
this_frame = mine.readline()
|
||||||
|
mine.close()
|
||||||
|
message = [{"cmd": "Bounce", "tags": ["Online"],
|
||||||
|
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
|
||||||
|
"spr": this_sprite, "frm": this_frame}}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
async def game_watcher(ctx: UndertaleContext):
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
await ctx.update_death_link(ctx.deathlink_status)
|
||||||
|
path = ctx.save_game_folder
|
||||||
|
if ctx.syncing:
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if ".item" in file:
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
sync_msg = [{"cmd": "Sync"}]
|
||||||
|
if ctx.locations_checked:
|
||||||
|
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||||
|
await ctx.send_msgs(sync_msg)
|
||||||
|
ctx.syncing = False
|
||||||
|
if ctx.got_deathlink:
|
||||||
|
ctx.got_deathlink = False
|
||||||
|
with open(os.path.join(ctx.save_game_folder, "/WelcomeToTheDead.youDied"), "w") as f:
|
||||||
|
f.close()
|
||||||
|
sending = []
|
||||||
|
victory = False
|
||||||
|
found_routes = 0
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if "DontBeMad.mad" in file and "DeathLink" in ctx.tags:
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
await ctx.send_death()
|
||||||
|
if "scout" == file:
|
||||||
|
sending = []
|
||||||
|
with open(root+"/"+file, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for l in lines:
|
||||||
|
if ctx.server_locations.__contains__(int(l)+12000):
|
||||||
|
sending = sending + [int(l)+12000]
|
||||||
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||||
|
"create_as_hint": int(2)}])
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
if "check.spot" in file:
|
||||||
|
sending = []
|
||||||
|
with open(root+"/"+file, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for l in lines:
|
||||||
|
sending = sending+[(int(l))+12000]
|
||||||
|
message = [{"cmd": "LocationChecks", "locations": sending}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
if "victory" in file and str(ctx.route) in file:
|
||||||
|
victory = True
|
||||||
|
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
if "victory" in file:
|
||||||
|
if str(ctx.route) == "all_routes":
|
||||||
|
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
||||||
|
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone neutral",
|
||||||
|
"default": 0, "want_reply": True, "operations": [{"operation": "max",
|
||||||
|
"value": 1}]}])
|
||||||
|
elif "pacifist" in file and ctx.completed_routes["pacifist"] != 1:
|
||||||
|
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone pacifist",
|
||||||
|
"default": 0, "want_reply": True, "operations": [{"operation": "max",
|
||||||
|
"value": 1}]}])
|
||||||
|
elif "genocide" in file and ctx.completed_routes["genocide"] != 1:
|
||||||
|
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone genocide",
|
||||||
|
"default": 0, "want_reply": True, "operations": [{"operation": "max",
|
||||||
|
"value": 1}]}])
|
||||||
|
if str(ctx.route) == "all_routes":
|
||||||
|
found_routes += ctx.completed_routes["neutral"]
|
||||||
|
found_routes += ctx.completed_routes["pacifist"]
|
||||||
|
found_routes += ctx.completed_routes["genocide"]
|
||||||
|
if str(ctx.route) == "all_routes" and found_routes >= 3:
|
||||||
|
victory = True
|
||||||
|
ctx.locations_checked = sending
|
||||||
|
if (not ctx.finished_game) and victory:
|
||||||
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
|
ctx.finished_game = True
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
Utils.init_logging("UndertaleClient", exception_logger="Client")
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
ctx = UndertaleContext(None, None)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
asyncio.create_task(
|
||||||
|
game_watcher(ctx), name="UndertaleProgressionWatcher")
|
||||||
|
|
||||||
|
asyncio.create_task(
|
||||||
|
multi_watcher(ctx), name="UndertaleMultiplayerWatcher")
|
||||||
|
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
asyncio.run(_main())
|
||||||
|
colorama.deinit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = get_base_parser(description="Undertale Client, for text interfacing.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
main()
|
|
@ -88,6 +88,7 @@ Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
|
||||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||||
Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
||||||
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
||||||
|
Name: "client/ut"; Description: "Undertale"; Types: full playing
|
||||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
|
@ -131,6 +132,7 @@ Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: i
|
||||||
Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
||||||
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
||||||
Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
||||||
|
Source: "{#source_path}\ArchipelagoUndertaleClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ut
|
||||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
|
@ -150,6 +152,7 @@ Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\Archip
|
||||||
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||||
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||||
Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
|
Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
|
||||||
|
Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut
|
||||||
|
|
||||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||||
|
@ -166,6 +169,7 @@ Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app
|
||||||
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
||||||
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
||||||
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||||
|
Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
|
class ItemData(typing.NamedTuple):
|
||||||
|
code: typing.Optional[int]
|
||||||
|
classification: any
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleItem(Item):
|
||||||
|
game: str = "Undertale"
|
||||||
|
|
||||||
|
|
||||||
|
item_table = {
|
||||||
|
"Progressive Plot": ItemData(77700, ItemClassification.progression),
|
||||||
|
"Progressive Weapons": ItemData(77701, ItemClassification.useful),
|
||||||
|
"Progressive Armor": ItemData(77702, ItemClassification.useful),
|
||||||
|
"Monster Candy": ItemData(77001, ItemClassification.filler),
|
||||||
|
"Croquet Roll": ItemData(77002, ItemClassification.filler),
|
||||||
|
"Stick": ItemData(77003, ItemClassification.useful),
|
||||||
|
"Bandage": ItemData(77004, ItemClassification.useful),
|
||||||
|
"Rock Candy": ItemData(77005, ItemClassification.filler),
|
||||||
|
"Pumpkin Rings": ItemData(77006, ItemClassification.filler),
|
||||||
|
"Spider Donut": ItemData(77007, ItemClassification.filler),
|
||||||
|
"Stoic Onion": ItemData(77008, ItemClassification.filler),
|
||||||
|
"Ghost Fruit": ItemData(77009, ItemClassification.filler),
|
||||||
|
"Spider Cider": ItemData(77010, ItemClassification.filler),
|
||||||
|
"Butterscotch Pie": ItemData(77011, ItemClassification.useful),
|
||||||
|
"Faded Ribbon": ItemData(77012, ItemClassification.useful),
|
||||||
|
"Toy Knife": ItemData(77013, ItemClassification.useful),
|
||||||
|
"Tough Glove": ItemData(77014, ItemClassification.useful),
|
||||||
|
"Manly Bandanna": ItemData(77015, ItemClassification.useful),
|
||||||
|
"Snowman Piece": ItemData(77016, ItemClassification.useful),
|
||||||
|
"Nice Cream": ItemData(77017, ItemClassification.filler),
|
||||||
|
"Puppydough Icecream": ItemData(77018, ItemClassification.filler),
|
||||||
|
"Bisicle": ItemData(77019, ItemClassification.filler),
|
||||||
|
"Unisicle": ItemData(77020, ItemClassification.filler),
|
||||||
|
"Cinnamon Bun": ItemData(77021, ItemClassification.filler),
|
||||||
|
"Temmie Flakes": ItemData(77022, ItemClassification.filler),
|
||||||
|
"Abandoned Quiche": ItemData(77023, ItemClassification.filler),
|
||||||
|
"Old Tutu": ItemData(77024, ItemClassification.useful),
|
||||||
|
"Ballet Shoes": ItemData(77025, ItemClassification.useful),
|
||||||
|
"Punch Card": ItemData(77026, ItemClassification.progression),
|
||||||
|
"Annoying Dog": ItemData(77027, ItemClassification.filler),
|
||||||
|
"Dog Salad": ItemData(77028, ItemClassification.filler),
|
||||||
|
"Dog Residue": ItemData(77029, ItemClassification.filler),
|
||||||
|
"Astronaut Food": ItemData(77035, ItemClassification.filler),
|
||||||
|
"Instant Noodles": ItemData(77036, ItemClassification.useful),
|
||||||
|
"Crab Apple": ItemData(77037, ItemClassification.filler),
|
||||||
|
"Hot Dog...?": ItemData(77038, ItemClassification.progression),
|
||||||
|
"Hot Cat": ItemData(77039, ItemClassification.filler),
|
||||||
|
"Glamburger": ItemData(77040, ItemClassification.filler),
|
||||||
|
"Sea Tea": ItemData(77041, ItemClassification.filler),
|
||||||
|
"Starfait": ItemData(77042, ItemClassification.filler),
|
||||||
|
"Legendary Hero": ItemData(77043, ItemClassification.filler),
|
||||||
|
"Cloudy Glasses": ItemData(77044, ItemClassification.useful),
|
||||||
|
"Torn Notebook": ItemData(77045, ItemClassification.useful),
|
||||||
|
"Stained Apron": ItemData(77046, ItemClassification.useful),
|
||||||
|
"Burnt Pan": ItemData(77047, ItemClassification.useful),
|
||||||
|
"Cowboy Hat": ItemData(77048, ItemClassification.useful),
|
||||||
|
"Empty Gun": ItemData(77049, ItemClassification.useful),
|
||||||
|
"Heart Locket": ItemData(77050, ItemClassification.useful),
|
||||||
|
"Worn Dagger": ItemData(77051, ItemClassification.useful),
|
||||||
|
"Real Knife": ItemData(77052, ItemClassification.useful),
|
||||||
|
"The Locket": ItemData(77053, ItemClassification.useful),
|
||||||
|
"Bad Memory": ItemData(77054, ItemClassification.filler),
|
||||||
|
"Dream": ItemData(77055, ItemClassification.filler),
|
||||||
|
"Undyne's Letter": ItemData(77056, ItemClassification.filler),
|
||||||
|
"Undyne Letter EX": ItemData(77057, ItemClassification.progression),
|
||||||
|
"Popato Chisps": ItemData(77058, ItemClassification.filler),
|
||||||
|
"Junk Food": ItemData(77059, ItemClassification.filler),
|
||||||
|
"Mystery Key": ItemData(77060, ItemClassification.filler),
|
||||||
|
"Face Steak": ItemData(77061, ItemClassification.filler),
|
||||||
|
"Hush Puppy": ItemData(77062, ItemClassification.filler),
|
||||||
|
"Snail Pie": ItemData(77063, ItemClassification.filler),
|
||||||
|
"temy armor": ItemData(77064, ItemClassification.useful),
|
||||||
|
"Complete Skeleton": ItemData(77779, ItemClassification.progression),
|
||||||
|
"Fish": ItemData(77780, ItemClassification.progression),
|
||||||
|
"DT Extractor": ItemData(77782, ItemClassification.progression),
|
||||||
|
"Mettaton Plush": ItemData(77786, ItemClassification.progression),
|
||||||
|
"Left Home Key": ItemData(77787, ItemClassification.progression),
|
||||||
|
"LOVE": ItemData(77788, ItemClassification.useful),
|
||||||
|
"Right Home Key": ItemData(77789, ItemClassification.progression),
|
||||||
|
"Key Piece": ItemData(77000, ItemClassification.progression),
|
||||||
|
"100G": ItemData(77999, ItemClassification.useful),
|
||||||
|
"500G": ItemData(77998, ItemClassification.useful),
|
||||||
|
"1000G": ItemData(77997, ItemClassification.progression),
|
||||||
|
"ATK Up": ItemData(77065, ItemClassification.useful),
|
||||||
|
"DEF Up": ItemData(77066, ItemClassification.useful),
|
||||||
|
"HP Up": ItemData(77067, ItemClassification.useful),
|
||||||
|
"FIGHT": ItemData(77077, ItemClassification.progression),
|
||||||
|
"ACT": ItemData(77078, ItemClassification.progression),
|
||||||
|
"ITEM": ItemData(77079, ItemClassification.progression),
|
||||||
|
"MERCY": ItemData(77080, ItemClassification.progression),
|
||||||
|
"Ruins Key": ItemData(77081, ItemClassification.progression),
|
||||||
|
"Snowdin Key": ItemData(77082, ItemClassification.progression),
|
||||||
|
"Waterfall Key": ItemData(77083, ItemClassification.progression),
|
||||||
|
"Hotland Key": ItemData(77084, ItemClassification.progression),
|
||||||
|
"Core Key": ItemData(77085, ItemClassification.progression),
|
||||||
|
"Undyne Date": ItemData(None, ItemClassification.progression),
|
||||||
|
"Alphys Date": ItemData(None, ItemClassification.progression),
|
||||||
|
"Papyrus Date": ItemData(None, ItemClassification.progression),
|
||||||
|
}
|
||||||
|
|
||||||
|
non_key_items = {
|
||||||
|
"Butterscotch Pie": 1,
|
||||||
|
"500G": 2,
|
||||||
|
"1000G": 2,
|
||||||
|
"Face Steak": 1,
|
||||||
|
"Snowman Piece": 1,
|
||||||
|
"Instant Noodles": 1,
|
||||||
|
"Astronaut Food": 2,
|
||||||
|
"Hot Cat": 1,
|
||||||
|
"Abandoned Quiche": 1,
|
||||||
|
"Spider Donut": 1,
|
||||||
|
"Spider Cider": 1,
|
||||||
|
"Hush Puppy": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
required_armor = {
|
||||||
|
"Cloudy Glasses": 1,
|
||||||
|
"Manly Bandanna": 1,
|
||||||
|
"Old Tutu": 1,
|
||||||
|
"Stained Apron": 1,
|
||||||
|
"Heart Locket": 1,
|
||||||
|
"Faded Ribbon": 1,
|
||||||
|
"Cowboy Hat": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
required_weapons = {
|
||||||
|
"Torn Notebook": 1,
|
||||||
|
"Tough Glove": 1,
|
||||||
|
"Ballet Shoes": 1,
|
||||||
|
"Burnt Pan": 1,
|
||||||
|
"Worn Dagger": 1,
|
||||||
|
"Toy Knife": 1,
|
||||||
|
"Empty Gun": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
plot_items = {
|
||||||
|
"Complete Skeleton": 1,
|
||||||
|
"Fish": 1,
|
||||||
|
"Mettaton Plush": 1,
|
||||||
|
"DT Extractor": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
key_items = {
|
||||||
|
"Complete Skeleton": 1,
|
||||||
|
"Fish": 1,
|
||||||
|
"DT Extractor": 1,
|
||||||
|
"Mettaton Plush": 1,
|
||||||
|
"Punch Card": 3,
|
||||||
|
"Hot Dog...?": 1,
|
||||||
|
"ATK Up": 19,
|
||||||
|
"DEF Up": 4,
|
||||||
|
"HP Up": 19,
|
||||||
|
"LOVE": 19,
|
||||||
|
"Ruins Key": 1,
|
||||||
|
"Snowdin Key": 1,
|
||||||
|
"Waterfall Key": 1,
|
||||||
|
"Hotland Key": 1,
|
||||||
|
"Core Key": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
junk_weights_all = {
|
||||||
|
"Bisicle": 12,
|
||||||
|
"Legendary Hero": 8,
|
||||||
|
"Glamburger": 10,
|
||||||
|
"Crab Apple": 12,
|
||||||
|
"Sea Tea": 12,
|
||||||
|
"Nice Cream": 10,
|
||||||
|
"Spider Donut": 10,
|
||||||
|
"Popato Chisps": 12,
|
||||||
|
"Junk Food": 12,
|
||||||
|
"Temmie Flakes": 10,
|
||||||
|
"Spider Cider": 8,
|
||||||
|
"Hot Dog...?": 10,
|
||||||
|
"Cinnamon Bun": 10,
|
||||||
|
"Starfait": 12,
|
||||||
|
"Punch Card": 8,
|
||||||
|
"Monster Candy": 6,
|
||||||
|
"100G": 6,
|
||||||
|
"500G": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
junk_weights_neutral = {
|
||||||
|
"Bisicle": 12,
|
||||||
|
"Legendary Hero": 8,
|
||||||
|
"Glamburger": 10,
|
||||||
|
"Crab Apple": 12,
|
||||||
|
"Sea Tea": 12,
|
||||||
|
"Nice Cream": 10,
|
||||||
|
"Spider Donut": 10,
|
||||||
|
"Junk Food": 12,
|
||||||
|
"Temmie Flakes": 10,
|
||||||
|
"Spider Cider": 8,
|
||||||
|
"Cinnamon Bun": 10,
|
||||||
|
"Starfait": 12,
|
||||||
|
"Punch Card": 8,
|
||||||
|
"Monster Candy": 6,
|
||||||
|
"100G": 6,
|
||||||
|
"500G": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
junk_weights_pacifist = {
|
||||||
|
"Bisicle": 12,
|
||||||
|
"Legendary Hero": 8,
|
||||||
|
"Glamburger": 10,
|
||||||
|
"Crab Apple": 12,
|
||||||
|
"Sea Tea": 12,
|
||||||
|
"Nice Cream": 10,
|
||||||
|
"Spider Donut": 10,
|
||||||
|
"Popato Chisps": 12,
|
||||||
|
"Junk Food": 12,
|
||||||
|
"Temmie Flakes": 10,
|
||||||
|
"Spider Cider": 8,
|
||||||
|
"Hot Dog...?": 10,
|
||||||
|
"Cinnamon Bun": 10,
|
||||||
|
"Starfait": 12,
|
||||||
|
"Punch Card": 8,
|
||||||
|
"Monster Candy": 6,
|
||||||
|
"100G": 6,
|
||||||
|
"500G": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
junk_weights_genocide = {
|
||||||
|
"Bisicle": 12,
|
||||||
|
"Legendary Hero": 8,
|
||||||
|
"Glamburger": 10,
|
||||||
|
"Crab Apple": 12,
|
||||||
|
"Sea Tea": 12,
|
||||||
|
"Spider Donut": 10,
|
||||||
|
"Junk Food": 12,
|
||||||
|
"Temmie Flakes": 10,
|
||||||
|
"Spider Cider": 8,
|
||||||
|
"Cinnamon Bun": 10,
|
||||||
|
"Starfait": 12,
|
||||||
|
"Monster Candy": 6,
|
||||||
|
"100G": 6,
|
||||||
|
"500G": 3,
|
||||||
|
}
|
|
@ -0,0 +1,376 @@
|
||||||
|
from BaseClasses import Location
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
|
class AdvData(typing.NamedTuple):
|
||||||
|
id: typing.Optional[int]
|
||||||
|
region: str
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleAdvancement(Location):
|
||||||
|
game: str = "Undertale"
|
||||||
|
|
||||||
|
def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
|
||||||
|
super().__init__(player, name, address, parent)
|
||||||
|
self.event = not address
|
||||||
|
|
||||||
|
|
||||||
|
advancement_table = {
|
||||||
|
"Snowman": AdvData(79100, "Snowdin Forest"),
|
||||||
|
"Snowman 2": AdvData(79101, "Snowdin Forest"),
|
||||||
|
"Snowman 3": AdvData(79102, "Snowdin Forest"),
|
||||||
|
"Nicecream Snowdin": AdvData(79001, "Snowdin Forest"),
|
||||||
|
"Nicecream Waterfall": AdvData(79002, "Waterfall"),
|
||||||
|
"Nicecream Punch Card": AdvData(79003, "Waterfall"),
|
||||||
|
"Quiche Bench": AdvData(79004, "Waterfall"),
|
||||||
|
"Tutu Hidden": AdvData(79005, "Waterfall"),
|
||||||
|
"Card Reward": AdvData(79006, "Waterfall"),
|
||||||
|
"Grass Shoes": AdvData(79007, "Waterfall"),
|
||||||
|
"Noodles Fridge": AdvData(79008, "Hotland"),
|
||||||
|
"Pan Hidden": AdvData(79009, "Hotland"),
|
||||||
|
"Apron Hidden": AdvData(79010, "Hotland"),
|
||||||
|
"Trash Burger": AdvData(79011, "Core"),
|
||||||
|
"Present Knife": AdvData(79012, "New Home"),
|
||||||
|
"Present Locket": AdvData(79013, "New Home"),
|
||||||
|
"Candy 1": AdvData(79014, "Ruins"),
|
||||||
|
"Candy 2": AdvData(79015, "Ruins"),
|
||||||
|
"Candy 3": AdvData(79016, "Ruins"),
|
||||||
|
"Candy 4": AdvData(79017, "Ruins"),
|
||||||
|
"Donut Sale": AdvData(79018, "Ruins"),
|
||||||
|
"Cider Sale": AdvData(79019, "Ruins"),
|
||||||
|
"Ribbon Cracks": AdvData(79020, "Ruins"),
|
||||||
|
"Toy Knife Edge": AdvData(79021, "Ruins"),
|
||||||
|
"B.Scotch Pie Given": AdvData(79022, "Ruins"),
|
||||||
|
"Astro 1": AdvData(79023, "Waterfall"),
|
||||||
|
"Astro 2": AdvData(79024, "Waterfall"),
|
||||||
|
"Dog Sale 1": AdvData(79026, "Hotland"),
|
||||||
|
"Cat Sale": AdvData(79027, "Hotland"),
|
||||||
|
"Dog Sale 2": AdvData(79028, "Hotland"),
|
||||||
|
"Dog Sale 3": AdvData(79029, "Hotland"),
|
||||||
|
"Dog Sale 4": AdvData(79030, "Hotland"),
|
||||||
|
"Chisps Machine": AdvData(79031, "True Lab"),
|
||||||
|
"Hush Trade": AdvData(79032, "Hotland"),
|
||||||
|
"Letter Quest": AdvData(79033, "Snowdin Town"),
|
||||||
|
"Bunny 1": AdvData(79034, "Snowdin Town"),
|
||||||
|
"Bunny 2": AdvData(79035, "Snowdin Town"),
|
||||||
|
"Bunny 3": AdvData(79036, "Snowdin Town"),
|
||||||
|
"Bunny 4": AdvData(79037, "Snowdin Town"),
|
||||||
|
"Gerson 1": AdvData(79038, "Waterfall"),
|
||||||
|
"Gerson 2": AdvData(79039, "Waterfall"),
|
||||||
|
"Gerson 3": AdvData(79040, "Waterfall"),
|
||||||
|
"Gerson 4": AdvData(79041, "Waterfall"),
|
||||||
|
"Bratty Catty 1": AdvData(79042, "Hotland"),
|
||||||
|
"Bratty Catty 2": AdvData(79043, "Hotland"),
|
||||||
|
"Bratty Catty 3": AdvData(79044, "Hotland"),
|
||||||
|
"Bratty Catty 4": AdvData(79045, "Hotland"),
|
||||||
|
"Burgerpants 1": AdvData(79046, "Hotland"),
|
||||||
|
"Burgerpants 2": AdvData(79047, "Hotland"),
|
||||||
|
"Burgerpants 3": AdvData(79048, "Hotland"),
|
||||||
|
"Burgerpants 4": AdvData(79049, "Hotland"),
|
||||||
|
"TemmieShop 1": AdvData(79050, "Waterfall"),
|
||||||
|
"TemmieShop 2": AdvData(79051, "Waterfall"),
|
||||||
|
"TemmieShop 3": AdvData(79052, "Waterfall"),
|
||||||
|
"TemmieShop 4": AdvData(79053, "Waterfall"),
|
||||||
|
"Papyrus Plot": AdvData(79056, "Snowdin Town"),
|
||||||
|
"Undyne Plot": AdvData(79057, "Waterfall"),
|
||||||
|
"Mettaton Plot": AdvData(79062, "Core"),
|
||||||
|
"True Lab Plot": AdvData(79063, "Hotland"),
|
||||||
|
"Left New Home Key": AdvData(79064, "New Home"),
|
||||||
|
"Right New Home Key": AdvData(79065, "New Home"),
|
||||||
|
"LOVE 2": AdvData(79902, "???"),
|
||||||
|
"LOVE 3": AdvData(79903, "???"),
|
||||||
|
"LOVE 4": AdvData(79904, "???"),
|
||||||
|
"LOVE 5": AdvData(79905, "???"),
|
||||||
|
"LOVE 6": AdvData(79906, "???"),
|
||||||
|
"LOVE 7": AdvData(79907, "???"),
|
||||||
|
"LOVE 8": AdvData(79908, "???"),
|
||||||
|
"LOVE 9": AdvData(79909, "???"),
|
||||||
|
"LOVE 10": AdvData(79910, "???"),
|
||||||
|
"LOVE 11": AdvData(79911, "???"),
|
||||||
|
"LOVE 12": AdvData(79912, "???"),
|
||||||
|
"LOVE 13": AdvData(79913, "???"),
|
||||||
|
"LOVE 14": AdvData(79914, "???"),
|
||||||
|
"LOVE 15": AdvData(79915, "???"),
|
||||||
|
"LOVE 16": AdvData(79916, "???"),
|
||||||
|
"LOVE 17": AdvData(79917, "???"),
|
||||||
|
"LOVE 18": AdvData(79918, "???"),
|
||||||
|
"LOVE 19": AdvData(79919, "???"),
|
||||||
|
"LOVE 20": AdvData(79920, "???"),
|
||||||
|
"ATK 2": AdvData(79800, "???"),
|
||||||
|
"ATK 3": AdvData(79801, "???"),
|
||||||
|
"ATK 4": AdvData(79802, "???"),
|
||||||
|
"ATK 5": AdvData(79803, "???"),
|
||||||
|
"ATK 6": AdvData(79804, "???"),
|
||||||
|
"ATK 7": AdvData(79805, "???"),
|
||||||
|
"ATK 8": AdvData(79806, "???"),
|
||||||
|
"ATK 9": AdvData(79807, "???"),
|
||||||
|
"ATK 10": AdvData(79808, "???"),
|
||||||
|
"ATK 11": AdvData(79809, "???"),
|
||||||
|
"ATK 12": AdvData(79810, "???"),
|
||||||
|
"ATK 13": AdvData(79811, "???"),
|
||||||
|
"ATK 14": AdvData(79812, "???"),
|
||||||
|
"ATK 15": AdvData(79813, "???"),
|
||||||
|
"ATK 16": AdvData(79814, "???"),
|
||||||
|
"ATK 17": AdvData(79815, "???"),
|
||||||
|
"ATK 18": AdvData(79816, "???"),
|
||||||
|
"ATK 19": AdvData(79817, "???"),
|
||||||
|
"ATK 20": AdvData(79818, "???"),
|
||||||
|
"DEF 5": AdvData(79700, "???"),
|
||||||
|
"DEF 9": AdvData(79701, "???"),
|
||||||
|
"DEF 13": AdvData(79702, "???"),
|
||||||
|
"DEF 17": AdvData(79703, "???"),
|
||||||
|
"HP 2": AdvData(79600, "???"),
|
||||||
|
"HP 3": AdvData(79601, "???"),
|
||||||
|
"HP 4": AdvData(79602, "???"),
|
||||||
|
"HP 5": AdvData(79603, "???"),
|
||||||
|
"HP 6": AdvData(79604, "???"),
|
||||||
|
"HP 7": AdvData(79605, "???"),
|
||||||
|
"HP 8": AdvData(79606, "???"),
|
||||||
|
"HP 9": AdvData(79607, "???"),
|
||||||
|
"HP 10": AdvData(79608, "???"),
|
||||||
|
"HP 11": AdvData(79609, "???"),
|
||||||
|
"HP 12": AdvData(79610, "???"),
|
||||||
|
"HP 13": AdvData(79611, "???"),
|
||||||
|
"HP 14": AdvData(79612, "???"),
|
||||||
|
"HP 15": AdvData(79613, "???"),
|
||||||
|
"HP 16": AdvData(79614, "???"),
|
||||||
|
"HP 17": AdvData(79615, "???"),
|
||||||
|
"HP 18": AdvData(79616, "???"),
|
||||||
|
"HP 19": AdvData(79617, "???"),
|
||||||
|
"HP 20": AdvData(79618, "???"),
|
||||||
|
"Undyne Date": AdvData(None, "Undyne\"s Home"),
|
||||||
|
"Alphys Date": AdvData(None, "Hotland"),
|
||||||
|
"Papyrus Date": AdvData(None, "Papyrus\" Home"),
|
||||||
|
}
|
||||||
|
|
||||||
|
exclusion_table = {
|
||||||
|
"pacifist": {
|
||||||
|
"LOVE 2",
|
||||||
|
"LOVE 3",
|
||||||
|
"LOVE 4",
|
||||||
|
"LOVE 5",
|
||||||
|
"LOVE 6",
|
||||||
|
"LOVE 7",
|
||||||
|
"LOVE 8",
|
||||||
|
"LOVE 9",
|
||||||
|
"LOVE 10",
|
||||||
|
"LOVE 11",
|
||||||
|
"LOVE 12",
|
||||||
|
"LOVE 13",
|
||||||
|
"LOVE 14",
|
||||||
|
"LOVE 15",
|
||||||
|
"LOVE 16",
|
||||||
|
"LOVE 17",
|
||||||
|
"LOVE 18",
|
||||||
|
"LOVE 19",
|
||||||
|
"LOVE 20",
|
||||||
|
"ATK 2",
|
||||||
|
"ATK 3",
|
||||||
|
"ATK 4",
|
||||||
|
"ATK 5",
|
||||||
|
"ATK 6",
|
||||||
|
"ATK 7",
|
||||||
|
"ATK 8",
|
||||||
|
"ATK 9",
|
||||||
|
"ATK 10",
|
||||||
|
"ATK 11",
|
||||||
|
"ATK 12",
|
||||||
|
"ATK 13",
|
||||||
|
"ATK 14",
|
||||||
|
"ATK 15",
|
||||||
|
"ATK 16",
|
||||||
|
"ATK 17",
|
||||||
|
"ATK 18",
|
||||||
|
"ATK 19",
|
||||||
|
"ATK 20",
|
||||||
|
"DEF 5",
|
||||||
|
"DEF 9",
|
||||||
|
"DEF 13",
|
||||||
|
"DEF 17",
|
||||||
|
"HP 2",
|
||||||
|
"HP 3",
|
||||||
|
"HP 4",
|
||||||
|
"HP 5",
|
||||||
|
"HP 6",
|
||||||
|
"HP 7",
|
||||||
|
"HP 8",
|
||||||
|
"HP 9",
|
||||||
|
"HP 10",
|
||||||
|
"HP 11",
|
||||||
|
"HP 12",
|
||||||
|
"HP 13",
|
||||||
|
"HP 14",
|
||||||
|
"HP 15",
|
||||||
|
"HP 16",
|
||||||
|
"HP 17",
|
||||||
|
"HP 18",
|
||||||
|
"HP 19",
|
||||||
|
"HP 20",
|
||||||
|
"Snowman 2",
|
||||||
|
"Snowman 3",
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"Letter Quest",
|
||||||
|
"Dog Sale 1",
|
||||||
|
"Cat Sale",
|
||||||
|
"Dog Sale 2",
|
||||||
|
"Dog Sale 3",
|
||||||
|
"Dog Sale 4",
|
||||||
|
"Chisps Machine",
|
||||||
|
"Hush Trade",
|
||||||
|
"LOVE 2",
|
||||||
|
"LOVE 3",
|
||||||
|
"LOVE 4",
|
||||||
|
"LOVE 5",
|
||||||
|
"LOVE 6",
|
||||||
|
"LOVE 7",
|
||||||
|
"LOVE 8",
|
||||||
|
"LOVE 9",
|
||||||
|
"LOVE 10",
|
||||||
|
"LOVE 11",
|
||||||
|
"LOVE 12",
|
||||||
|
"LOVE 13",
|
||||||
|
"LOVE 14",
|
||||||
|
"LOVE 15",
|
||||||
|
"LOVE 16",
|
||||||
|
"LOVE 17",
|
||||||
|
"LOVE 18",
|
||||||
|
"LOVE 19",
|
||||||
|
"LOVE 20",
|
||||||
|
"Papyrus Plot",
|
||||||
|
"Undyne Plot",
|
||||||
|
"True Lab Plot",
|
||||||
|
"ATK 2",
|
||||||
|
"ATK 3",
|
||||||
|
"ATK 4",
|
||||||
|
"ATK 5",
|
||||||
|
"ATK 6",
|
||||||
|
"ATK 7",
|
||||||
|
"ATK 8",
|
||||||
|
"ATK 9",
|
||||||
|
"ATK 10",
|
||||||
|
"ATK 11",
|
||||||
|
"ATK 12",
|
||||||
|
"ATK 13",
|
||||||
|
"ATK 14",
|
||||||
|
"ATK 15",
|
||||||
|
"ATK 16",
|
||||||
|
"ATK 17",
|
||||||
|
"ATK 18",
|
||||||
|
"ATK 19",
|
||||||
|
"ATK 20",
|
||||||
|
"DEF 5",
|
||||||
|
"DEF 9",
|
||||||
|
"DEF 13",
|
||||||
|
"DEF 17",
|
||||||
|
"HP 2",
|
||||||
|
"HP 3",
|
||||||
|
"HP 4",
|
||||||
|
"HP 5",
|
||||||
|
"HP 6",
|
||||||
|
"HP 7",
|
||||||
|
"HP 8",
|
||||||
|
"HP 9",
|
||||||
|
"HP 10",
|
||||||
|
"HP 11",
|
||||||
|
"HP 12",
|
||||||
|
"HP 13",
|
||||||
|
"HP 14",
|
||||||
|
"HP 15",
|
||||||
|
"HP 16",
|
||||||
|
"HP 17",
|
||||||
|
"HP 18",
|
||||||
|
"HP 19",
|
||||||
|
"HP 20",
|
||||||
|
"Snowman 2",
|
||||||
|
"Snowman 3",
|
||||||
|
},
|
||||||
|
"genocide": {
|
||||||
|
"Letter Quest",
|
||||||
|
"Dog Sale 1",
|
||||||
|
"Cat Sale",
|
||||||
|
"Dog Sale 2",
|
||||||
|
"Dog Sale 3",
|
||||||
|
"Dog Sale 4",
|
||||||
|
"Chisps Machine",
|
||||||
|
"Nicecream Snowdin",
|
||||||
|
"Nicecream Waterfall",
|
||||||
|
"Nicecream Punch Card",
|
||||||
|
"Card Reward",
|
||||||
|
"Apron Hidden",
|
||||||
|
"Hush Trade",
|
||||||
|
"Papyrus Plot",
|
||||||
|
"Undyne Plot",
|
||||||
|
"True Lab Plot",
|
||||||
|
},
|
||||||
|
"NoLove": {
|
||||||
|
"LOVE 2",
|
||||||
|
"LOVE 3",
|
||||||
|
"LOVE 4",
|
||||||
|
"LOVE 5",
|
||||||
|
"LOVE 6",
|
||||||
|
"LOVE 7",
|
||||||
|
"LOVE 8",
|
||||||
|
"LOVE 9",
|
||||||
|
"LOVE 10",
|
||||||
|
"LOVE 11",
|
||||||
|
"LOVE 12",
|
||||||
|
"LOVE 13",
|
||||||
|
"LOVE 14",
|
||||||
|
"LOVE 15",
|
||||||
|
"LOVE 16",
|
||||||
|
"LOVE 17",
|
||||||
|
"LOVE 18",
|
||||||
|
"LOVE 19",
|
||||||
|
"LOVE 20",
|
||||||
|
},
|
||||||
|
"NoStats": {
|
||||||
|
"ATK 2",
|
||||||
|
"ATK 3",
|
||||||
|
"ATK 4",
|
||||||
|
"ATK 5",
|
||||||
|
"ATK 6",
|
||||||
|
"ATK 7",
|
||||||
|
"ATK 8",
|
||||||
|
"ATK 9",
|
||||||
|
"ATK 10",
|
||||||
|
"ATK 11",
|
||||||
|
"ATK 12",
|
||||||
|
"ATK 13",
|
||||||
|
"ATK 14",
|
||||||
|
"ATK 15",
|
||||||
|
"ATK 16",
|
||||||
|
"ATK 17",
|
||||||
|
"ATK 18",
|
||||||
|
"ATK 19",
|
||||||
|
"ATK 20",
|
||||||
|
"DEF 5",
|
||||||
|
"DEF 9",
|
||||||
|
"DEF 13",
|
||||||
|
"DEF 17",
|
||||||
|
"HP 2",
|
||||||
|
"HP 3",
|
||||||
|
"HP 4",
|
||||||
|
"HP 5",
|
||||||
|
"HP 6",
|
||||||
|
"HP 7",
|
||||||
|
"HP 8",
|
||||||
|
"HP 9",
|
||||||
|
"HP 10",
|
||||||
|
"HP 11",
|
||||||
|
"HP 12",
|
||||||
|
"HP 13",
|
||||||
|
"HP 14",
|
||||||
|
"HP 15",
|
||||||
|
"HP 16",
|
||||||
|
"HP 17",
|
||||||
|
"HP 18",
|
||||||
|
"HP 19",
|
||||||
|
"HP 20",
|
||||||
|
},
|
||||||
|
"all_routes": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events_table = {
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import typing
|
||||||
|
from Options import Choice, Option, Toggle, Range
|
||||||
|
|
||||||
|
|
||||||
|
class RouteRequired(Choice):
|
||||||
|
"""Main route of the game required to win."""
|
||||||
|
display_name = "Required Route"
|
||||||
|
option_neutral = 0
|
||||||
|
option_pacifist = 1
|
||||||
|
option_genocide = 2
|
||||||
|
option_all_routes = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class IncludeTemy(Toggle):
|
||||||
|
"""Adds Temmy Armor to the item pool."""
|
||||||
|
display_name = "Include Temy Armor"
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class KeyPieces(Range):
|
||||||
|
"""How many Key Pieces are added to the pool, only matters with Key Piece Hunt enabled."""
|
||||||
|
display_name = "Key Piece Amount"
|
||||||
|
default = 5
|
||||||
|
range_start = 1
|
||||||
|
range_end = 10
|
||||||
|
|
||||||
|
|
||||||
|
class KeyHunt(Toggle):
|
||||||
|
"""Adds Key Pieces to the item pool, you need all of them to enter the last corridor."""
|
||||||
|
display_name = "Key Piece Hunt"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressiveArmor(Toggle):
|
||||||
|
"""Makes the armor progressive."""
|
||||||
|
display_name = "Progressive Armor"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressiveWeapons(Toggle):
|
||||||
|
"""Makes the weapons progressive."""
|
||||||
|
display_name = "Progressive Weapons"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class OnlyFlakes(Toggle):
|
||||||
|
"""Replaces all non-required items, except equipment, with Temmie Flakes."""
|
||||||
|
display_name = "Only Temmie Flakes"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class NoEquips(Toggle):
|
||||||
|
"""Removes all equippable items."""
|
||||||
|
display_name = "No Equippables"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeLove(Toggle):
|
||||||
|
"""Adds LOVE to the pool. GENOCIDE ONLY!"""
|
||||||
|
display_name = "Randomize LOVE"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeStats(Toggle):
|
||||||
|
"""Makes each stat increase from LV a separate item. GENOCIDE ONLY!
|
||||||
|
Warning: This tends to spam chat with sending out checks."""
|
||||||
|
display_name = "Randomize Stats"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class RandoBattleOptions(Toggle):
|
||||||
|
"""Turns the ITEM button in battle into an item you have to receive."""
|
||||||
|
display_name = "Randomize Item Button"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
undertale_options: typing.Dict[str, type(Option)] = {
|
||||||
|
"route_required": RouteRequired,
|
||||||
|
"key_hunt": KeyHunt,
|
||||||
|
"key_pieces": KeyPieces,
|
||||||
|
"rando_love": RandomizeLove,
|
||||||
|
"rando_stats": RandomizeStats,
|
||||||
|
"temy_include": IncludeTemy,
|
||||||
|
"no_equips": NoEquips,
|
||||||
|
"only_flakes": OnlyFlakes,
|
||||||
|
"prog_armor": ProgressiveArmor,
|
||||||
|
"prog_weapons": ProgressiveWeapons,
|
||||||
|
"rando_item_button": RandoBattleOptions,
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
from BaseClasses import MultiWorld
|
||||||
|
|
||||||
|
|
||||||
|
def link_undertale_areas(world: MultiWorld, player: int):
|
||||||
|
for (exit, region) in mandatory_connections:
|
||||||
|
world.get_entrance(exit, player).connect(world.get_region(region, player))
|
||||||
|
|
||||||
|
|
||||||
|
# (Region name, list of exits)
|
||||||
|
undertale_regions = [
|
||||||
|
("Menu", ["New Game", "??? Exit"]),
|
||||||
|
("???", []),
|
||||||
|
("Hub", ["Ruins Hub", "Snowdin Hub", "Waterfall Hub", "Hotland Hub", "Core Hub"]),
|
||||||
|
("Ruins", ["Ruins Exit"]),
|
||||||
|
("Old Home", []),
|
||||||
|
("Snowdin Forest", ["Snowdin Forest Exit"]),
|
||||||
|
("Snowdin Town", ["Papyrus\" Home Entrance"]),
|
||||||
|
("Papyrus\" Home", []),
|
||||||
|
("Waterfall", ["Undyne\"s Home Entrance"]),
|
||||||
|
("Undyne\"s Home", []),
|
||||||
|
("Hotland", ["Cooking Show Entrance", "Lab Elevator"]),
|
||||||
|
("Cooking Show", ["News Show Entrance"]),
|
||||||
|
("News Show", []),
|
||||||
|
("True Lab", []),
|
||||||
|
("Core", ["Core Exit"]),
|
||||||
|
("New Home", ["New Home Exit"]),
|
||||||
|
("Barrier", []),
|
||||||
|
]
|
||||||
|
|
||||||
|
# (Entrance, region pointed to)
|
||||||
|
mandatory_connections = [
|
||||||
|
("??? Exit", "???"),
|
||||||
|
("New Game", "Hub"),
|
||||||
|
("Ruins Hub", "Ruins"),
|
||||||
|
("Ruins Exit", "Old Home"),
|
||||||
|
("Snowdin Forest Exit", "Snowdin Town"),
|
||||||
|
("Papyrus\" Home Entrance", "Papyrus\" Home"),
|
||||||
|
("Undyne\"s Home Entrance", "Undyne\"s Home"),
|
||||||
|
("Cooking Show Entrance", "Cooking Show"),
|
||||||
|
("News Show Entrance", "News Show"),
|
||||||
|
("Lab Elevator", "True Lab"),
|
||||||
|
("Core Exit", "New Home"),
|
||||||
|
("New Home Exit", "Barrier"),
|
||||||
|
("Snowdin Hub", "Snowdin Forest"),
|
||||||
|
("Waterfall Hub", "Waterfall"),
|
||||||
|
("Hotland Hub", "Hotland"),
|
||||||
|
("Core Hub", "Core"),
|
||||||
|
]
|
|
@ -0,0 +1,360 @@
|
||||||
|
from ..generic.Rules import set_rule, add_rule
|
||||||
|
from BaseClasses import MultiWorld, CollectionState
|
||||||
|
|
||||||
|
|
||||||
|
def _undertale_is_route(state: CollectionState, player: int, route: int):
|
||||||
|
if route == 3:
|
||||||
|
return state.multiworld.route_required[player].current_key == "all_routes"
|
||||||
|
if state.multiworld.route_required[player].current_key == "all_routes":
|
||||||
|
return True
|
||||||
|
if route == 0:
|
||||||
|
return state.multiworld.route_required[player].current_key == "neutral"
|
||||||
|
if route == 1:
|
||||||
|
return state.multiworld.route_required[player].current_key == "pacifist"
|
||||||
|
if route == 2:
|
||||||
|
return state.multiworld.route_required[player].current_key == "genocide"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _undertale_has_plot(state: CollectionState, player: int, item: str):
|
||||||
|
if item == "Complete Skeleton":
|
||||||
|
return state.has("Complete Skeleton", player)
|
||||||
|
elif item == "Fish":
|
||||||
|
return state.has("Fish", player)
|
||||||
|
elif item == "Mettaton Plush":
|
||||||
|
return state.has("Mettaton Plush", player)
|
||||||
|
elif item == "DT Extractor":
|
||||||
|
return state.has("DT Extractor", player)
|
||||||
|
|
||||||
|
|
||||||
|
def _undertale_can_level(state: CollectionState, exp: int, lvl: int):
|
||||||
|
if exp >= 10 and lvl == 1:
|
||||||
|
return True
|
||||||
|
elif exp >= 30 and lvl == 2:
|
||||||
|
return True
|
||||||
|
elif exp >= 70 and lvl == 3:
|
||||||
|
return True
|
||||||
|
elif exp >= 120 and lvl == 4:
|
||||||
|
return True
|
||||||
|
elif exp >= 200 and lvl == 5:
|
||||||
|
return True
|
||||||
|
elif exp >= 300 and lvl == 6:
|
||||||
|
return True
|
||||||
|
elif exp >= 500 and lvl == 7:
|
||||||
|
return True
|
||||||
|
elif exp >= 800 and lvl == 8:
|
||||||
|
return True
|
||||||
|
elif exp >= 1200 and lvl == 9:
|
||||||
|
return True
|
||||||
|
elif exp >= 1700 and lvl == 10:
|
||||||
|
return True
|
||||||
|
elif exp >= 2500 and lvl == 11:
|
||||||
|
return True
|
||||||
|
elif exp >= 3500 and lvl == 12:
|
||||||
|
return True
|
||||||
|
elif exp >= 5000 and lvl == 13:
|
||||||
|
return True
|
||||||
|
elif exp >= 7000 and lvl == 14:
|
||||||
|
return True
|
||||||
|
elif exp >= 10000 and lvl == 15:
|
||||||
|
return True
|
||||||
|
elif exp >= 15000 and lvl == 16:
|
||||||
|
return True
|
||||||
|
elif exp >= 25000 and lvl == 17:
|
||||||
|
return True
|
||||||
|
elif exp >= 50000 and lvl == 18:
|
||||||
|
return True
|
||||||
|
elif exp >= 99999 and lvl == 19:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Sets rules on entrances and advancements that are always applied
|
||||||
|
def set_rules(multiworld: MultiWorld, player: int):
|
||||||
|
set_rule(multiworld.get_entrance("Ruins Hub", player), lambda state: state.has("Ruins Key", player))
|
||||||
|
set_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("Snowdin Key", player))
|
||||||
|
set_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("Waterfall Key", player))
|
||||||
|
set_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: state.has("Hotland Key", player))
|
||||||
|
set_rule(multiworld.get_entrance("Core Hub", player), lambda state: state.has("Core Key", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 1):
|
||||||
|
add_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
add_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
add_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
add_rule(multiworld.get_entrance("Core Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 2) or _undertale_is_route(multiworld.state, player, 3):
|
||||||
|
add_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("FIGHT", player))
|
||||||
|
add_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("FIGHT", player))
|
||||||
|
add_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: state.has("FIGHT", player))
|
||||||
|
add_rule(multiworld.get_entrance("Core Hub", player), lambda state: state.has("FIGHT", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 0):
|
||||||
|
add_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
add_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
add_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
add_rule(multiworld.get_entrance("Core Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
set_rule(multiworld.get_entrance("Core Exit", player),
|
||||||
|
lambda state: _undertale_has_plot(state, player, "Mettaton Plush"))
|
||||||
|
set_rule(multiworld.get_entrance("New Home Exit", player),
|
||||||
|
lambda state: (state.has("Left Home Key", player) and
|
||||||
|
state.has("Right Home Key", player)) or
|
||||||
|
state.has("Key Piece", player, state.multiworld.key_pieces[player]))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 1):
|
||||||
|
set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player),
|
||||||
|
lambda state: _undertale_has_plot(state, player, "Complete Skeleton"))
|
||||||
|
set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player),
|
||||||
|
lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player))
|
||||||
|
set_rule(multiworld.get_entrance("Lab Elevator", player),
|
||||||
|
lambda state: state.has("Alphys Date", player) and _undertale_has_plot(state, player, "DT Extractor"))
|
||||||
|
set_rule(multiworld.get_location("Alphys Date", player),
|
||||||
|
lambda state: state.has("Undyne Letter EX", player) and state.has("Undyne Date", player))
|
||||||
|
set_rule(multiworld.get_location("Papyrus Plot", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Undyne Plot", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("True Lab Plot", player),
|
||||||
|
lambda state: state.can_reach("New Home", "Region", player)
|
||||||
|
and state.can_reach("Letter Quest", "Location", player))
|
||||||
|
set_rule(multiworld.get_location("Chisps Machine", player),
|
||||||
|
lambda state: state.can_reach("True Lab", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Dog Sale 1", player),
|
||||||
|
lambda state: state.can_reach("Cooking Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Cat Sale", player),
|
||||||
|
lambda state: state.can_reach("Cooking Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Dog Sale 2", player),
|
||||||
|
lambda state: state.can_reach("Cooking Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Dog Sale 3", player),
|
||||||
|
lambda state: state.can_reach("Cooking Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Dog Sale 4", player),
|
||||||
|
lambda state: state.can_reach("Cooking Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Hush Trade", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1))
|
||||||
|
set_rule(multiworld.get_location("Letter Quest", player),
|
||||||
|
lambda state: state.can_reach("New Home Exit", "Entrance", player))
|
||||||
|
if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3):
|
||||||
|
set_rule(multiworld.get_location("Nicecream Punch Card", player),
|
||||||
|
lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Nicecream Snowdin", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Nicecream Waterfall", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Card Reward", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Apron Hidden", player),
|
||||||
|
lambda state: state.can_reach("Cooking Show", "Region", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 2) and \
|
||||||
|
(multiworld.rando_love[player] or multiworld.rando_stats[player]):
|
||||||
|
maxlv = 1
|
||||||
|
exp = 190
|
||||||
|
curarea = "Old Home"
|
||||||
|
|
||||||
|
while maxlv < 20:
|
||||||
|
maxlv += 1
|
||||||
|
if multiworld.rando_love[player]:
|
||||||
|
set_rule(multiworld.get_location(("LOVE " + str(maxlv)), player), lambda state: False)
|
||||||
|
if multiworld.rando_stats[player]:
|
||||||
|
set_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: False)
|
||||||
|
set_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: False)
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
set_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: False)
|
||||||
|
maxlv = 1
|
||||||
|
while maxlv < 20:
|
||||||
|
while _undertale_can_level(multiworld.state, exp, maxlv):
|
||||||
|
maxlv += 1
|
||||||
|
if multiworld.rando_stats[player]:
|
||||||
|
if curarea == "Old Home":
|
||||||
|
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
|
||||||
|
add_rule(multiworld.get_location(("HP "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
|
||||||
|
elif curarea == "Snowdin Town":
|
||||||
|
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or")
|
||||||
|
add_rule(multiworld.get_location(("HP "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or")
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or")
|
||||||
|
elif curarea == "Waterfall":
|
||||||
|
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or")
|
||||||
|
add_rule(multiworld.get_location(("HP "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or")
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or")
|
||||||
|
elif curarea == "News Show":
|
||||||
|
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("News Show", "Region", player)), combine="or")
|
||||||
|
add_rule(multiworld.get_location(("HP "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("News Show", "Region", player)), combine="or")
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("News Show", "Region", player)), combine="or")
|
||||||
|
elif curarea == "Core":
|
||||||
|
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or")
|
||||||
|
add_rule(multiworld.get_location(("HP "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or")
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or")
|
||||||
|
elif curarea == "Sans":
|
||||||
|
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or")
|
||||||
|
add_rule(multiworld.get_location(("HP "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or")
|
||||||
|
if maxlv == 9 or maxlv == 13 or maxlv == 17:
|
||||||
|
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or")
|
||||||
|
if multiworld.rando_love[player]:
|
||||||
|
if curarea == "Old Home":
|
||||||
|
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
|
||||||
|
lambda state: ( state.can_reach("Old Home", "Region", player)), combine="or")
|
||||||
|
elif curarea == "Snowdin Town":
|
||||||
|
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or")
|
||||||
|
elif curarea == "Waterfall":
|
||||||
|
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or")
|
||||||
|
elif curarea == "News Show":
|
||||||
|
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("News Show", "Region", player)), combine="or")
|
||||||
|
elif curarea == "Core":
|
||||||
|
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or")
|
||||||
|
elif curarea == "Sans":
|
||||||
|
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
|
||||||
|
lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or")
|
||||||
|
if curarea == "Old Home":
|
||||||
|
curarea = "Snowdin Town"
|
||||||
|
maxlv = 1
|
||||||
|
exp = 407
|
||||||
|
elif curarea == "Snowdin Town":
|
||||||
|
curarea = "Waterfall"
|
||||||
|
maxlv = 1
|
||||||
|
exp = 1643
|
||||||
|
elif curarea == "Waterfall":
|
||||||
|
curarea = "News Show"
|
||||||
|
maxlv = 1
|
||||||
|
exp = 3320
|
||||||
|
elif curarea == "News Show":
|
||||||
|
curarea = "Core"
|
||||||
|
maxlv = 1
|
||||||
|
exp = 50000
|
||||||
|
elif curarea == "Core":
|
||||||
|
curarea = "Sans"
|
||||||
|
maxlv = 1
|
||||||
|
exp = 99999
|
||||||
|
set_rule(multiworld.get_entrance("??? Exit", player), lambda state: state.has("FIGHT", player))
|
||||||
|
set_rule(multiworld.get_location("Snowman", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 1):
|
||||||
|
set_rule(multiworld.get_location("Donut Sale", player),
|
||||||
|
lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
set_rule(multiworld.get_location("Cider Sale", player),
|
||||||
|
lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
set_rule(multiworld.get_location("Ribbon Cracks", player),
|
||||||
|
lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
set_rule(multiworld.get_location("Toy Knife Edge", player),
|
||||||
|
lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
set_rule(multiworld.get_location("B.Scotch Pie Given", player),
|
||||||
|
lambda state: state.has("ACT", player) and state.has("MERCY", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 2) or _undertale_is_route(multiworld.state, player, 3):
|
||||||
|
set_rule(multiworld.get_location("Donut Sale", player),
|
||||||
|
lambda state: state.has("FIGHT", player))
|
||||||
|
set_rule(multiworld.get_location("Cider Sale", player),
|
||||||
|
lambda state: state.has("FIGHT", player))
|
||||||
|
set_rule(multiworld.get_location("Ribbon Cracks", player),
|
||||||
|
lambda state: state.has("FIGHT", player))
|
||||||
|
set_rule(multiworld.get_location("Toy Knife Edge", player),
|
||||||
|
lambda state: state.has("FIGHT", player))
|
||||||
|
set_rule(multiworld.get_location("B.Scotch Pie Given", player),
|
||||||
|
lambda state: state.has("FIGHT", player))
|
||||||
|
if _undertale_is_route(multiworld.state, player, 0):
|
||||||
|
set_rule(multiworld.get_location("Donut Sale", player),
|
||||||
|
lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
set_rule(multiworld.get_location("Cider Sale", player),
|
||||||
|
lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
set_rule(multiworld.get_location("Ribbon Cracks", player),
|
||||||
|
lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
set_rule(multiworld.get_location("Toy Knife Edge", player),
|
||||||
|
lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
set_rule(multiworld.get_location("B.Scotch Pie Given", player),
|
||||||
|
lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player)))
|
||||||
|
set_rule(multiworld.get_location("Mettaton Plot", player),
|
||||||
|
lambda state: state.can_reach("Core Exit", "Entrance", player))
|
||||||
|
set_rule(multiworld.get_location("Bunny 1", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bunny 2", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bunny 3", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bunny 4", player),
|
||||||
|
lambda state: state.can_reach("Snowdin Town", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Astro 1", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Astro 2", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Gerson 1", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Gerson 2", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Gerson 3", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Gerson 4", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Present Knife", player),
|
||||||
|
lambda state: state.can_reach("New Home", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Present Locket", player),
|
||||||
|
lambda state: state.can_reach("New Home", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Left New Home Key", player),
|
||||||
|
lambda state: state.can_reach("New Home", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Right New Home Key", player),
|
||||||
|
lambda state: state.can_reach("New Home", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Trash Burger", player),
|
||||||
|
lambda state: state.can_reach("Core", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Quiche Bench", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Tutu Hidden", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Grass Shoes", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("TemmieShop 1", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("TemmieShop 2", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("TemmieShop 3", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("TemmieShop 4", player),
|
||||||
|
lambda state: state.can_reach("Waterfall", "Region", player) and state.has("1000G", player, 2))
|
||||||
|
set_rule(multiworld.get_location("Noodles Fridge", player),
|
||||||
|
lambda state: state.can_reach("Hotland", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Pan Hidden", player),
|
||||||
|
lambda state: state.can_reach("Hotland", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bratty Catty 1", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bratty Catty 2", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bratty Catty 3", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Bratty Catty 4", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Burgerpants 1", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Burgerpants 2", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Burgerpants 3", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
set_rule(multiworld.get_location("Burgerpants 4", player),
|
||||||
|
lambda state: state.can_reach("News Show", "Region", player))
|
||||||
|
|
||||||
|
|
||||||
|
# Sets rules on completion condition
|
||||||
|
def set_completion_rules(multiworld: MultiWorld, player: int):
|
||||||
|
completion_requirements = lambda state: state.can_reach("New Home Exit", "Entrance", player) and state.has("FIGHT", player)
|
||||||
|
if _undertale_is_route(multiworld.state, player, 1):
|
||||||
|
completion_requirements = lambda state: state.can_reach("True Lab", "Region", player)
|
||||||
|
|
||||||
|
multiworld.completion_condition[player] = lambda state: completion_requirements(state)
|
|
@ -0,0 +1,228 @@
|
||||||
|
from .Items import UndertaleItem, item_table, required_armor, required_weapons, non_key_items, key_items, \
|
||||||
|
junk_weights_all, plot_items, junk_weights_neutral, junk_weights_pacifist, junk_weights_genocide
|
||||||
|
from .Locations import UndertaleAdvancement, advancement_table, exclusion_table
|
||||||
|
from .Regions import undertale_regions, link_undertale_areas
|
||||||
|
from .Rules import set_rules, set_completion_rules
|
||||||
|
from worlds.generic.Rules import exclusion_rules
|
||||||
|
from BaseClasses import Region, Entrance, Tutorial, Item
|
||||||
|
from .Options import undertale_options
|
||||||
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
from worlds.LauncherComponents import Component, components, Type
|
||||||
|
from multiprocessing import Process
|
||||||
|
|
||||||
|
|
||||||
|
def run_client():
|
||||||
|
print('running undertale client')
|
||||||
|
from UndertaleClient import main # lazy import
|
||||||
|
p = Process(target=main)
|
||||||
|
p.start()
|
||||||
|
|
||||||
|
|
||||||
|
components.append(Component("Undertale Client", "UndertaleClient"))
|
||||||
|
|
||||||
|
|
||||||
|
def data_path(file_name: str):
|
||||||
|
import pkgutil
|
||||||
|
return pkgutil.get_data(__name__, "data/" + file_name)
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleWeb(WebWorld):
|
||||||
|
tutorials = [Tutorial(
|
||||||
|
"Multiworld Setup Tutorial",
|
||||||
|
"A guide to setting up the Archipelago Undertale software on your computer. This guide covers "
|
||||||
|
"single-player, multiworld, and related software.",
|
||||||
|
"English",
|
||||||
|
"undertale_en.md",
|
||||||
|
"undertale/en",
|
||||||
|
["Mewlif"]
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleWorld(World):
|
||||||
|
"""
|
||||||
|
Undertale is an RPG where every choice you make matters. You could choose to hurt all the enemies, eventually
|
||||||
|
causing genocide of the monster species. Or you can spare all the enemies, befriending them and freeing them
|
||||||
|
from their underground prison.
|
||||||
|
"""
|
||||||
|
game = "Undertale"
|
||||||
|
option_definitions = undertale_options
|
||||||
|
topology_present = True
|
||||||
|
web = UndertaleWeb()
|
||||||
|
|
||||||
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
|
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
|
||||||
|
|
||||||
|
data_version = 5
|
||||||
|
|
||||||
|
def _get_undertale_data(self):
|
||||||
|
return {
|
||||||
|
"world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32),
|
||||||
|
"seed_name": self.multiworld.seed_name,
|
||||||
|
"player_name": self.multiworld.get_player_name(self.player),
|
||||||
|
"player_id": self.player,
|
||||||
|
"client_version": self.required_client_version,
|
||||||
|
"race": self.multiworld.is_race,
|
||||||
|
"route": self.multiworld.route_required[self.player].current_key,
|
||||||
|
"temy_armor_include": bool(self.multiworld.temy_include[self.player].value),
|
||||||
|
"only_flakes": bool(self.multiworld.only_flakes[self.player].value),
|
||||||
|
"no_equips": bool(self.multiworld.no_equips[self.player].value),
|
||||||
|
"key_hunt": bool(self.multiworld.key_hunt[self.player].value),
|
||||||
|
"key_pieces": self.multiworld.key_pieces[self.player].value,
|
||||||
|
"rando_love": bool(self.multiworld.rando_love[self.player].value),
|
||||||
|
"rando_stats": bool(self.multiworld.rando_stats[self.player].value),
|
||||||
|
"prog_armor": bool(self.multiworld.prog_armor[self.player].value),
|
||||||
|
"prog_weapons": bool(self.multiworld.prog_weapons[self.player].value),
|
||||||
|
"rando_item_button": bool(self.multiworld.rando_item_button[self.player].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_items(self):
|
||||||
|
self.multiworld.get_location("Undyne Date", self.player).place_locked_item(self.create_item("Undyne Date"))
|
||||||
|
self.multiworld.get_location("Alphys Date", self.player).place_locked_item(self.create_item("Alphys Date"))
|
||||||
|
self.multiworld.get_location("Papyrus Date", self.player).place_locked_item(self.create_item("Papyrus Date"))
|
||||||
|
# Generate item pool
|
||||||
|
itempool = []
|
||||||
|
if self.multiworld.route_required[self.player] == "all_routes":
|
||||||
|
junk_pool = junk_weights_all.copy()
|
||||||
|
elif self.multiworld.route_required[self.player] == "genocide":
|
||||||
|
junk_pool = junk_weights_genocide.copy()
|
||||||
|
elif self.multiworld.route_required[self.player] == "neutral":
|
||||||
|
junk_pool = junk_weights_neutral.copy()
|
||||||
|
elif self.multiworld.route_required[self.player] == "pacifist":
|
||||||
|
junk_pool = junk_weights_pacifist.copy()
|
||||||
|
else:
|
||||||
|
junk_pool = junk_weights_all.copy()
|
||||||
|
# Add all required progression items
|
||||||
|
for name, num in key_items.items():
|
||||||
|
itempool += [name] * num
|
||||||
|
for name, num in required_armor.items():
|
||||||
|
itempool += [name] * num
|
||||||
|
for name, num in required_weapons.items():
|
||||||
|
itempool += [name] * num
|
||||||
|
for name, num in non_key_items.items():
|
||||||
|
itempool += [name] * num
|
||||||
|
if self.multiworld.rando_item_button[self.player]:
|
||||||
|
itempool += ["ITEM"]
|
||||||
|
else:
|
||||||
|
self.multiworld.push_precollected(self.create_item("ITEM"))
|
||||||
|
self.multiworld.push_precollected(self.create_item("FIGHT"))
|
||||||
|
self.multiworld.push_precollected(self.create_item("ACT"))
|
||||||
|
chosen_key_start = self.multiworld.per_slot_randoms[self.player].choice(["Ruins Key", "Snowdin Key", "Waterfall Key", "Hotland Key"])
|
||||||
|
self.multiworld.push_precollected(self.create_item(chosen_key_start))
|
||||||
|
itempool.remove(chosen_key_start)
|
||||||
|
self.multiworld.push_precollected(self.create_item("MERCY"))
|
||||||
|
if self.multiworld.route_required[self.player] == "genocide":
|
||||||
|
itempool = [item for item in itempool if item != "Popato Chisps" and item != "Stained Apron" and
|
||||||
|
item != "Nice Cream" and item != "Hot Cat" and item != "Hot Dog...?" and item != "Punch Card"]
|
||||||
|
elif self.multiworld.route_required[self.player] == "neutral":
|
||||||
|
itempool = [item for item in itempool if item != "Popato Chisps" and item != "Hot Cat" and
|
||||||
|
item != "Hot Dog...?"]
|
||||||
|
if self.multiworld.route_required[self.player] == "pacifist" or \
|
||||||
|
self.multiworld.route_required[self.player] == "all_routes":
|
||||||
|
itempool += ["Undyne Letter EX"]
|
||||||
|
else:
|
||||||
|
itempool.remove("Complete Skeleton")
|
||||||
|
itempool.remove("Fish")
|
||||||
|
itempool.remove("DT Extractor")
|
||||||
|
itempool.remove("Hush Puppy")
|
||||||
|
if self.multiworld.key_hunt[self.player]:
|
||||||
|
itempool += ["Key Piece"] * self.multiworld.key_pieces[self.player].value
|
||||||
|
else:
|
||||||
|
itempool += ["Left Home Key"]
|
||||||
|
itempool += ["Right Home Key"]
|
||||||
|
if not self.multiworld.rando_love[self.player] or \
|
||||||
|
(self.multiworld.route_required[self.player] != "genocide" and
|
||||||
|
self.multiworld.route_required[self.player] != "all_routes"):
|
||||||
|
itempool = [item for item in itempool if not item == "LOVE"]
|
||||||
|
if not self.multiworld.rando_stats[self.player] or \
|
||||||
|
(self.multiworld.route_required[self.player] != "genocide" and
|
||||||
|
self.multiworld.route_required[self.player] != "all_routes"):
|
||||||
|
itempool = [item for item in itempool if not (item == "ATK Up" or item == "DEF Up" or item == "HP Up")]
|
||||||
|
if self.multiworld.temy_include[self.player]:
|
||||||
|
itempool += ["temy armor"]
|
||||||
|
if self.multiworld.no_equips[self.player]:
|
||||||
|
itempool = [item for item in itempool if item not in required_armor and item not in required_weapons]
|
||||||
|
else:
|
||||||
|
if self.multiworld.prog_armor[self.player]:
|
||||||
|
itempool = [item if (item not in required_armor and not item == "temy armor") else
|
||||||
|
"Progressive Armor" for item in itempool]
|
||||||
|
if self.multiworld.prog_weapons[self.player]:
|
||||||
|
itempool = [item if item not in required_weapons else "Progressive Weapons" for item in itempool]
|
||||||
|
if self.multiworld.route_required[self.player] == "genocide" or \
|
||||||
|
self.multiworld.route_required[self.player] == "all_routes":
|
||||||
|
if not self.multiworld.only_flakes[self.player]:
|
||||||
|
itempool += ["Snowman Piece"] * 2
|
||||||
|
if not self.multiworld.no_equips[self.player]:
|
||||||
|
itempool = ["Real Knife" if item == "Worn Dagger" else "The Locket"
|
||||||
|
if item == "Heart Locket" else item for item in itempool]
|
||||||
|
if self.multiworld.only_flakes[self.player]:
|
||||||
|
itempool = [item for item in itempool if item not in non_key_items]
|
||||||
|
# Choose locations to automatically exclude based on settings
|
||||||
|
exclusion_pool = set()
|
||||||
|
exclusion_pool.update(exclusion_table[self.multiworld.route_required[self.player].current_key])
|
||||||
|
if not self.multiworld.rando_love[self.player] or \
|
||||||
|
(self.multiworld.route_required[self.player] != "genocide" and
|
||||||
|
self.multiworld.route_required[self.player] != "all_routes"):
|
||||||
|
exclusion_pool.update(exclusion_table["NoLove"])
|
||||||
|
if not self.multiworld.rando_stats[self.player] or \
|
||||||
|
(self.multiworld.route_required[self.player] != "genocide" and
|
||||||
|
self.multiworld.route_required[self.player] != "all_routes"):
|
||||||
|
exclusion_pool.update(exclusion_table["NoStats"])
|
||||||
|
|
||||||
|
# Choose locations to automatically exclude based on settings
|
||||||
|
exclusion_checks = set()
|
||||||
|
exclusion_checks.update(["Nicecream Punch Card", "Hush Trade"])
|
||||||
|
exclusion_rules(self.multiworld, self.player, exclusion_checks)
|
||||||
|
|
||||||
|
# Fill remaining items with randomly generated junk or Temmie Flakes
|
||||||
|
if not self.multiworld.only_flakes[self.player]:
|
||||||
|
itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
|
||||||
|
k=len(self.location_names)-len(itempool)-len(exclusion_pool))
|
||||||
|
else:
|
||||||
|
itempool += ["Temmie Flakes"] * (len(self.location_names) - len(itempool) - len(exclusion_pool))
|
||||||
|
# Convert itempool into real items
|
||||||
|
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
|
||||||
|
|
||||||
|
self.multiworld.itempool += itempool
|
||||||
|
|
||||||
|
def set_rules(self):
|
||||||
|
set_rules(self.multiworld, self.player)
|
||||||
|
set_completion_rules(self.multiworld, self.player)
|
||||||
|
|
||||||
|
def create_regions(self):
|
||||||
|
def UndertaleRegion(region_name: str, exits=[]):
|
||||||
|
ret = Region(region_name, self.player, self.multiworld)
|
||||||
|
ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
|
||||||
|
for loc_name, loc_data in advancement_table.items()
|
||||||
|
if loc_data.region == region_name and
|
||||||
|
(loc_name not in exclusion_table["NoStats"] or
|
||||||
|
(self.multiworld.rando_stats[self.player] and
|
||||||
|
(self.multiworld.route_required[self.player] == "genocide" or
|
||||||
|
self.multiworld.route_required[self.player] == "all_routes"))) and
|
||||||
|
(loc_name not in exclusion_table["NoLove"] or
|
||||||
|
(self.multiworld.rando_love[self.player] and
|
||||||
|
(self.multiworld.route_required[self.player] == "genocide" or
|
||||||
|
self.multiworld.route_required[self.player] == "all_routes"))) and
|
||||||
|
loc_name not in exclusion_table[self.multiworld.route_required[self.player].current_key]]
|
||||||
|
for exit in exits:
|
||||||
|
ret.exits.append(Entrance(self.player, exit, ret))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
self.multiworld.regions += [UndertaleRegion(*r) for r in undertale_regions]
|
||||||
|
link_undertale_areas(self.multiworld, self.player)
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
slot_data = self._get_undertale_data()
|
||||||
|
for option_name in undertale_options:
|
||||||
|
option = getattr(self.multiworld, option_name)[self.player]
|
||||||
|
if (option_name == "rando_love" or option_name == "rando_stats") and \
|
||||||
|
self.multiworld.route_required[self.player] != "genocide" and \
|
||||||
|
self.multiworld.route_required[self.player] != "all_routes":
|
||||||
|
option.value = False
|
||||||
|
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
|
||||||
|
slot_data[option_name] = int(option.value)
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def create_item(self, name: str) -> Item:
|
||||||
|
item_data = item_table[name]
|
||||||
|
item = UndertaleItem(name, item_data.classification, item_data.code, self.player)
|
||||||
|
return item
|
Binary file not shown.
|
@ -0,0 +1,21 @@
|
||||||
|
# Undertale
|
||||||
|
|
||||||
|
## Where is the settings page?
|
||||||
|
|
||||||
|
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||||
|
config file.
|
||||||
|
|
||||||
|
## What is considered a location check in Undertale?
|
||||||
|
|
||||||
|
Location checks in Undertale are all the spots in the game where you can get an item. Exceptions are Dog Residue,
|
||||||
|
the Nicecream bought in Hotland, and anything you cannot get in your chosen route.
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
|
||||||
|
When the player receives an item in Undertale, it will go into their inventory if they have space, otherwise it will
|
||||||
|
wait until they do have space. That includes items that don't appear in your inventory.
|
||||||
|
|
||||||
|
## What is the victory condition?
|
||||||
|
|
||||||
|
Victory is achieved when the player completes their chosen route. If they chose `all_routes` then they need to complete
|
||||||
|
every major route in the game, those being `Pacifist`, `Neutral`, and `Genocide`.
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Undertale Randomizer Setup Guide
|
||||||
|
|
||||||
|
### Required Software
|
||||||
|
|
||||||
|
- Undertale from the [Steam page](https://store.steampowered.com/app/391540)
|
||||||
|
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
- (select `Undertale Client` during installation.)
|
||||||
|
|
||||||
|
### First time setup
|
||||||
|
|
||||||
|
Start the Undertale client, and in the bottom text box, input `/auto_patch (Input your Undertale install directory here)` (It is usually located at `C:\Program Files\Steam\steamapps\Undertale`, but it can be different, you can more easily find the directory
|
||||||
|
by opening the Undertale directory through Steam), it will then make an Undertale folder that will be created in the
|
||||||
|
Archipelago install location. That contains the version of Undertale you will use for Archipelago. (You will need to
|
||||||
|
redo this step when updating Archipelago.)
|
||||||
|
|
||||||
|
### Connect to the MultiServer
|
||||||
|
|
||||||
|
Make sure both Undertale and its client are running. (Undertale will ask for a saveslot, it can be 1 through 99, none
|
||||||
|
of the slots will overwrite your vanilla save, although you may want to make a backup just in case.)
|
||||||
|
|
||||||
|
In the top text box of the client, type the
|
||||||
|
`Ip Address` (or `Hostname`) and `Port` separated with a `:` symbol. (Ex. `archipelago.gg:38281`)
|
||||||
|
|
||||||
|
The client will then ask for the slot name, input that in the text box at the bottom of the client.
|
||||||
|
|
||||||
|
### Play the game
|
||||||
|
|
||||||
|
When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
|
||||||
|
multiworld game!
|
||||||
|
|
||||||
|
### Where do I get a YAML file?
|
||||||
|
|
||||||
|
You can customize your settings by visiting the [Undertale Player Settings Page](/games/Undertale/player-settings)
|
Loading…
Reference in New Issue