Undertale: Fixes a major logic bug, and updates Undertale to use the new Options API (#3528)

* Updated the options definitions to the new api

* Fixed the wrong base class being used for UndertaleOptions

* Undertale: Added get_filler_item_name to Undertale, changed multiworld.per_slot_randoms to self.random, removed some unused imports in options.py, and fixed rules.py still using state.multiworld instead of world.options, and simplified the set_completion_rules function in rules.py

* Undertale: Fixed it trying to add strings to the finished item pool

* fixed 1000g item not being in the key items pool for Undertale

* Removed ".copy()" for the junk_weights, reformatted the requested lines to have less new lines, and changed "itempool += [self.create_filler()]" to "itempool.append(self.create_filler())"
This commit is contained in:
Mewlif 2024-06-21 12:21:46 -04:00 committed by GitHub
parent ce37bed7c6
commit 40c9dfd3bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 136 additions and 118 deletions

View File

@ -105,7 +105,6 @@ item_table = {
non_key_items = {
"Butterscotch Pie": 1,
"500G": 2,
"1000G": 2,
"Face Steak": 1,
"Snowman Piece": 1,
"Instant Noodles": 1,
@ -147,6 +146,7 @@ plot_items = {
key_items = {
"Complete Skeleton": 1,
"Fish": 1,
"1000G": 2,
"DT Extractor": 1,
"Mettaton Plush": 1,
"Punch Card": 3,

View File

@ -1,5 +1,5 @@
import typing
from Options import Choice, Option, Toggle, Range
from Options import Choice, Toggle, Range, PerGameCommonOptions
from dataclasses import dataclass
class RouteRequired(Choice):
@ -86,17 +86,17 @@ class RandoBattleOptions(Toggle):
default = 0
undertale_options: typing.Dict[str, type(Option)] = {
"route_required": RouteRequired,
"starting_area": StartingArea,
"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,
}
@dataclass
class UndertaleOptions(PerGameCommonOptions):
route_required: RouteRequired
starting_area: StartingArea
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

@ -1,18 +1,22 @@
from worlds.generic.Rules import set_rule, add_rule, add_item_rule
from BaseClasses import MultiWorld, CollectionState
from worlds.generic.Rules import set_rule, add_rule
from BaseClasses import CollectionState
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import UndertaleWorld
def _undertale_is_route(state: CollectionState, player: int, route: int):
def _undertale_is_route(world: "UndertaleWorld", 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 world.options.route_required.current_key == "all_routes"
if world.options.route_required.current_key == "all_routes":
return True
if route == 0:
return state.multiworld.route_required[player].current_key == "neutral"
return world.options.route_required.current_key == "neutral"
if route == 1:
return state.multiworld.route_required[player].current_key == "pacifist"
return world.options.route_required.current_key == "pacifist"
if route == 2:
return state.multiworld.route_required[player].current_key == "genocide"
return world.options.route_required.current_key == "genocide"
return False
@ -27,7 +31,7 @@ def _undertale_has_plot(state: CollectionState, player: int, item: str):
return state.has("DT Extractor", player)
def _undertale_can_level(state: CollectionState, exp: int, lvl: int):
def _undertale_can_level(exp: int, lvl: int):
if exp >= 10 and lvl == 1:
return True
elif exp >= 30 and lvl == 2:
@ -70,7 +74,9 @@ def _undertale_can_level(state: CollectionState, exp: int, lvl: int):
# Sets rules on entrances and advancements that are always applied
def set_rules(multiworld: MultiWorld, player: int):
def set_rules(world: "UndertaleWorld"):
player = world.player
multiworld = world.multiworld
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))
@ -81,16 +87,16 @@ def set_rules(multiworld: MultiWorld, player: int):
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].value))
if _undertale_is_route(multiworld.state, player, 1):
state.has("Key Piece", player, world.options.key_pieces.value))
if _undertale_is_route(world, 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 state.has("DT Extractor", player) and
((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or
state.has("Key Piece", player, state.multiworld.key_pieces[player].value)))
((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or
state.has("Key Piece", player, world.options.key_pieces.value)))
set_rule(multiworld.get_location("Alphys Date", player),
lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player)
and state.has("Undyne Date", player))
@ -101,7 +107,10 @@ def set_rules(multiworld: MultiWorld, player: int):
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)
and state.can_reach("Alphys Date", "Location", player))
and state.can_reach("Alphys Date", "Location", player)
and ((state.has("Left Home Key", player) and
state.has("Right Home Key", player)) or
state.has("Key Piece", player, world.options.key_pieces.value)))
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),
@ -118,7 +127,7 @@ def set_rules(multiworld: MultiWorld, player: int):
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("Last Corridor", "Region", player) and state.has("Undyne Date", player))
if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3):
if (not _undertale_is_route(world, 2)) or _undertale_is_route(world, 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),
@ -129,26 +138,26 @@ def set_rules(multiworld: MultiWorld, player: int):
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 \
(bool(multiworld.rando_love[player].value) or bool(multiworld.rando_stats[player].value)):
if _undertale_is_route(world, 2) and \
(bool(world.options.rando_love.value) or bool(world.options.rando_stats.value)):
maxlv = 1
exp = 190
curarea = "Old Home"
while maxlv < 20:
maxlv += 1
if multiworld.rando_love[player]:
if world.options.rando_love:
set_rule(multiworld.get_location(("LOVE " + str(maxlv)), player), lambda state: False)
if multiworld.rando_stats[player]:
if world.options.rando_stats:
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 in {5, 9, 13, 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):
while _undertale_can_level(exp, maxlv):
maxlv += 1
if multiworld.rando_stats[player]:
if world.options.rando_stats:
if curarea == "Old Home":
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
@ -197,7 +206,7 @@ def set_rules(multiworld: MultiWorld, player: int):
if maxlv == 5 or 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 world.options.rando_love:
if curarea == "Old Home":
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
@ -307,9 +316,9 @@ def set_rules(multiworld: MultiWorld, player: int):
# Sets rules on completion condition
def set_completion_rules(multiworld: MultiWorld, player: int):
completion_requirements = lambda state: state.can_reach("Barrier", "Region", 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)
def set_completion_rules(world: "UndertaleWorld"):
player = world.player
multiworld = world.multiworld
multiworld.completion_condition[player] = lambda state: state.can_reach("Barrier", "Region", player)
if _undertale_is_route(world, 1):
multiworld.completion_condition[player] = lambda state: state.can_reach("True Lab", "Region", player)

View File

@ -5,9 +5,9 @@ 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 .Options import UndertaleOptions
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, Type
from worlds.LauncherComponents import Component, components
from multiprocessing import Process
@ -46,7 +46,8 @@ class UndertaleWorld(World):
from their underground prison.
"""
game = "Undertale"
option_definitions = undertale_options
options_dataclass = UndertaleOptions
options: UndertaleOptions
web = UndertaleWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
@ -54,39 +55,55 @@ class UndertaleWorld(World):
def _get_undertale_data(self):
return {
"world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32),
"world_seed": self.random.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,
"starting_area": self.multiworld.starting_area[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)
"route": self.options.route_required.current_key,
"starting_area": self.options.starting_area.current_key,
"temy_armor_include": bool(self.options.temy_include.value),
"only_flakes": bool(self.options.only_flakes.value),
"no_equips": bool(self.options.no_equips.value),
"key_hunt": bool(self.options.key_hunt.value),
"key_pieces": self.options.key_pieces.value,
"rando_love": bool(self.options.rando_love.value),
"rando_stats": bool(self.options.rando_stats.value),
"prog_armor": bool(self.options.prog_armor.value),
"prog_weapons": bool(self.options.prog_weapons.value),
"rando_item_button": bool(self.options.rando_item_button.value)
}
def get_filler_item_name(self):
if self.options.route_required == "all_routes":
junk_pool = junk_weights_all
elif self.options.route_required == "genocide":
junk_pool = junk_weights_genocide
elif self.options.route_required == "neutral":
junk_pool = junk_weights_neutral
elif self.options.route_required == "pacifist":
junk_pool = junk_weights_pacifist
else:
junk_pool = junk_weights_all
if not self.options.only_flakes:
return self.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()))[0]
else:
return "Temmie Flakes"
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":
if self.options.route_required == "all_routes":
junk_pool = junk_weights_all.copy()
elif self.multiworld.route_required[self.player] == "genocide":
elif self.options.route_required == "genocide":
junk_pool = junk_weights_genocide.copy()
elif self.multiworld.route_required[self.player] == "neutral":
elif self.options.route_required == "neutral":
junk_pool = junk_weights_neutral.copy()
elif self.multiworld.route_required[self.player] == "pacifist":
elif self.options.route_required == "pacifist":
junk_pool = junk_weights_pacifist.copy()
else:
junk_pool = junk_weights_all.copy()
@ -99,73 +116,68 @@ class UndertaleWorld(World):
itempool += [name] * num
for name, num in non_key_items.items():
itempool += [name] * num
if self.multiworld.rando_item_button[self.player]:
if self.options.rando_item_button:
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"))
self.multiworld.push_precollected(self.create_item("MERCY"))
if self.multiworld.route_required[self.player] == "genocide":
if self.options.route_required == "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":
elif self.options.route_required == "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":
if self.options.route_required == "pacifist" or self.options.route_required == "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
if self.options.key_hunt:
itempool += ["Key Piece"] * self.options.key_pieces.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"):
if not self.options.rando_love or \
(self.options.route_required != "genocide" and self.options.route_required != "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"):
if not self.options.rando_stats or \
(self.options.route_required != "genocide" and self.options.route_required != "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]:
if self.options.temy_include:
itempool += ["temy armor"]
if self.multiworld.no_equips[self.player]:
if self.options.no_equips:
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]:
if self.options.prog_armor:
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]:
if self.options.prog_weapons:
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]:
if self.options.route_required == "genocide" or \
self.options.route_required == "all_routes":
if not self.options.only_flakes:
itempool += ["Snowman Piece"] * 2
if not self.multiworld.no_equips[self.player]:
if not self.options.no_equips:
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]:
if self.options.only_flakes:
itempool = [item for item in itempool if item not in non_key_items]
starting_key = self.multiworld.starting_area[self.player].current_key.title() + " Key"
starting_key = self.options.starting_area.current_key.title() + " Key"
itempool.remove(starting_key)
self.multiworld.push_precollected(self.create_item(starting_key))
# 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[self.options.route_required.current_key])
if not self.options.rando_love or \
(self.options.route_required != "genocide" and self.options.route_required != "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"):
if not self.options.rando_stats or \
(self.options.route_required != "genocide" and self.options.route_required != "all_routes"):
exclusion_pool.update(exclusion_table["NoStats"])
# Choose locations to automatically exclude based on settings
@ -173,36 +185,33 @@ class UndertaleWorld(World):
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)]
# Fill remaining items with randomly generated junk or Temmie Flakes
while len(itempool) < len(self.multiworld.get_unfilled_locations(self.player)):
itempool.append(self.create_filler())
self.multiworld.itempool += itempool
def set_rules(self):
set_rules(self.multiworld, self.player)
set_completion_rules(self.multiworld, self.player)
set_rules(self)
set_completion_rules(self)
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 loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name and
(loc_name not in exclusion_table["NoStats"] or
(self.options.rando_stats and
(self.options.route_required == "genocide" or
self.options.route_required == "all_routes"))) and
(loc_name not in exclusion_table["NoLove"] or
(self.options.rando_love and
(self.options.route_required == "genocide" or
self.options.route_required == "all_routes"))) and
loc_name not in exclusion_table[self.options.route_required.current_key]]
for exit in exits:
ret.exits.append(Entrance(self.player, exit, ret))
return ret
@ -212,11 +221,11 @@ class UndertaleWorld(World):
def fill_slot_data(self):
slot_data = self._get_undertale_data()
for option_name in undertale_options:
for option_name in self.options.as_dict():
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":
self.options.route_required != "genocide" and \
self.options.route_required != "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)