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:
Mewlif 2023-06-26 22:35:41 -04:00 committed by GitHub
parent 71bfb6babd
commit 553fe0be19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1904 additions and 0 deletions

4
.gitignore vendored
View File

@ -28,6 +28,7 @@
*.apsave
*.BIN
setups
build
bundle/components.wxs
dist
@ -176,6 +177,9 @@ minecraft_versions.json
# pyenv
.python-version
#undertale stuff
/Undertale/
# OS General Files
.DS_Store
.AppleDouble

View File

@ -45,6 +45,7 @@ Currently, the following games are supported:
* Adventure
* DLC Quest
* Noita
* Undertale
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

498
UndertaleClient.py Normal file
View File

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

View File

@ -88,6 +88,7 @@ Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
Name: "client/zl"; Description: "Zillion"; Types: full playing
Name: "client/tloz"; Description: "The Legend of Zelda"; 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
[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}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
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
[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} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
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} 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} 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} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
[Run]

241
worlds/undertale/Items.py Normal file
View File

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

View File

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

View File

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

View File

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

360
worlds/undertale/Rules.py Normal file
View File

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

View File

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

View File

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

View File

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