Compare commits

..

1 Commits

Author SHA1 Message Date
NewSoupVi
80f85ca6f6 Update test_options.py 2024-11-16 01:36:28 +01:00
36 changed files with 632 additions and 755 deletions

View File

@@ -710,11 +710,6 @@ class CommonContext:
def run_cli(self):
if sys.stdin:
if sys.stdin.fileno() != 0:
from multiprocessing import parent_process
if parent_process():
return # ignore MultiProcessing pipe
# steam overlay breaks when starting console_loop
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")

View File

@@ -453,10 +453,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
if not isinstance(ret.game, str):
if ret.game is None:
raise Exception('"game" not specified')
raise Exception(f"Invalid game: {ret.game}")
if ret.game not in AutoWorldRegister.world_types:
from worlds import failed_world_loads
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]

View File

@@ -22,15 +22,16 @@ from os.path import isfile
from shutil import which
from typing import Callable, Optional, Sequence, Tuple, Union
import Utils
import settings
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
import settings
import Utils
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
user_path)
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
is_windows, is_macos, is_linux
def open_host_yaml():
@@ -181,11 +182,6 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
App.get_running_app().stop()
Window.close()
def _stop(self, *largs):
# see run_gui Launcher _stop comment for details
self.root_window.close()
super()._stop(*largs)
Popup().run()

View File

@@ -727,15 +727,15 @@ class Context:
if not hint.local and data not in concerns[hint.finding_player]:
concerns[hint.finding_player].append(data)
# remember hints in all cases
# since hints are bidirectional, finding player and receiving player,
# we can check once if hint already exists
if hint not in self.hints[team, hint.finding_player]:
self.hints[team, hint.finding_player].add(hint)
new_hint_events.add(hint.finding_player)
for player in self.slot_set(hint.receiving_player):
self.hints[team, player].add(hint)
new_hint_events.add(player)
if not hint.found:
# since hints are bidirectional, finding player and receiving player,
# we can check once if hint already exists
if hint not in self.hints[team, hint.finding_player]:
self.hints[team, hint.finding_player].add(hint)
new_hint_events.add(hint.finding_player)
for player in self.slot_set(hint.receiving_player):
self.hints[team, player].add(hint)
new_hint_events.add(player)
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
for slot in new_hint_events:

View File

@@ -18,7 +18,6 @@ import warnings
from argparse import Namespace
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard
from yaml import load, load_all, dump
@@ -569,8 +568,6 @@ def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
else:
if text:
queue.put_nowait(text)
else:
sleep(0.01) # non-blocking stream
from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)

View File

@@ -98,8 +98,6 @@
<td>
{% if hint.finding_player == player %}
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
{% elif get_slot_info(team, hint.finding_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
{% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
{{ player_names_with_alias[(team, hint.finding_player)] }}
@@ -109,8 +107,6 @@
<td>
{% if hint.receiving_player == player %}
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
{% elif get_slot_info(team, hint.receiving_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
{% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
{{ player_names_with_alias[(team, hint.receiving_player)] }}

View File

@@ -21,20 +21,8 @@
)
-%}
<tr>
<td>
{% if get_slot_info(team, hint.finding_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
{% else %}
{{ player_names_with_alias[(team, hint.finding_player)] }}
{% endif %}
</td>
<td>
{% if get_slot_info(team, hint.receiving_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
{% else %}
{{ player_names_with_alias[(team, hint.receiving_player)] }}
{% endif %}
</td>
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td>
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td>
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
<td>{{ games[(team, hint.finding_player)] }}</td>

View File

@@ -423,7 +423,6 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) ->
template_name_or_list="genericTracker.html",
game_specific_tracker=game in _player_trackers,
room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
team=team,
player=player,
player_name=tracker_data.get_room_long_player_names()[team, player],
@@ -447,7 +446,6 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
enabled_trackers=enabled_trackers,
current_tracker="Generic",
room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(),
@@ -499,7 +497,7 @@ if "Factorio" in network_data_package["games"]:
(team, player): collections.Counter({
tracker_data.item_id_to_name["Factorio"][item_id]: count
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
}) for team, players in tracker_data.get_all_players().items() for player in players
}) for team, players in tracker_data.get_all_slots().items() for player in players
if tracker_data.get_player_game(team, player) == "Factorio"
}
@@ -508,7 +506,6 @@ if "Factorio" in network_data_package["games"]:
enabled_trackers=enabled_trackers,
current_tracker="Factorio",
room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(),
@@ -641,7 +638,6 @@ if "A Link to the Past" in network_data_package["games"]:
enabled_trackers=enabled_trackers,
current_tracker="A Link to the Past",
room=tracker_data.room,
get_slot_info=tracker_data.get_slot_info,
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(),

View File

@@ -1,6 +1,5 @@
import unittest
from BaseClasses import PlandoOptions
from worlds import AutoWorldRegister
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
@@ -15,10 +14,6 @@ class TestOptionPresets(unittest.TestCase):
with self.subTest(game=game_name, preset=preset_name, option=option_name):
try:
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
# some options may need verification to ensure the provided option is actually valid
# pass in all plando options in case a preset wants to require certain plando options
# for some reason
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
if not any([issubclass(option.__class__, t) for t in supported_types]):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "

View File

@@ -740,20 +740,17 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region):
for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1):
# Matches the naming convention and iteration order in `create_rift_connections()`.
i = 1
while i <= len(rift_access_regions[time_rift.name]):
name = f"{time_rift.name} Portal - Entrance {i}"
entrance: Entrance
try:
entrance = world.get_entrance(name)
# Reconnect the rift access region to the new exit region.
entrance = world.multiworld.get_entrance(name, world.player)
reconnect_regions(entrance, entrance.parent_region, exit_region)
except KeyError:
# The original entrance to the time rift has been deleted by already reconnecting a telescope act to the
# time rift, so create a new entrance from the original rift access region to the new exit region.
# Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but
# starting acts/rifts and act-plando can reconnect acts to time rifts before this happens.
world.get_region(access_region).connect(exit_region, name)
time_rift.connect(exit_region, name)
i += 1
def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]:

View File

@@ -1152,79 +1152,79 @@ class AquariaRegions:
def __no_progression_hard_or_hidden_location(self) -> None:
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Sun Temple boss area, beating Sun God",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Sunken City, bulb on top of the boss area",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Home Water, Nautilus Egg",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Mithalas City Castle, beating the Priests",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Mermog cave, Piranha Egg",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Octopus Cave, Dumbo Egg",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Sun Worm path, first cliff bulb",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Sun Worm path, second cliff bulb",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Bubble Cave, Verse Egg",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Sun Temple, Sun Key",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("The Body bottom area, Mutant Costume",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
self.player).item_rule = \
lambda item: not item.advancement
lambda item: item.classification != ItemClassification.progression
def adjusting_rules(self, options: AquariaOptions) -> None:
"""

View File

@@ -117,13 +117,16 @@ class AquariaWorld(World):
Create an AquariaItem using 'name' as item name.
"""
result: AquariaItem
data = item_table[name]
classification: ItemClassification = ItemClassification.useful
if data.type == ItemType.JUNK:
classification = ItemClassification.filler
elif data.type == ItemType.PROGRESSION:
classification = ItemClassification.progression
result = AquariaItem(name, classification, data.id, self.player)
try:
data = item_table[name]
classification: ItemClassification = ItemClassification.useful
if data.type == ItemType.JUNK:
classification = ItemClassification.filler
elif data.type == ItemType.PROGRESSION:
classification = ItemClassification.progression
result = AquariaItem(name, classification, data.id, self.player)
except BaseException:
raise Exception('The item ' + name + ' is not valid.')
return result

View File

@@ -49,7 +49,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
for location in self.unfillable_locations:
for item_name in self.world.item_names:
item = self.get_item_by_name(item_name)
if item.advancement:
if item.classification == ItemClassification.progression:
self.assertFalse(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")

View File

@@ -105,8 +105,8 @@ function on_player_changed_position(event)
end
local target_direction = exit_table[outbound_direction]
local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
(CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
(CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
target_position = character.surface.find_non_colliding_position(character.prototype.name,
target_position, 32, 0.5)
if target_position ~= nil then
@@ -134,96 +134,40 @@ end
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
{% endif %}
function count_energy_bridges()
local count = 0
for i, bridge in pairs(storage.energy_link_bridges) do
if validate_energy_link_bridge(i, bridge) then
count = count + 1 + (bridge.quality.level * 0.3)
end
end
return count
end
function get_energy_increment(bridge)
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
end
function on_check_energy_link(event)
--- assuming 1 MJ increment and 5MJ battery:
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
if event.tick % 60 == 30 then
local surface = game.get_surface(1)
local force = "player"
local bridges = storage.energy_link_bridges
local bridgecount = count_energy_bridges()
local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force})
local bridgecount = table_size(bridges)
storage.forcedata[force].energy_bridges = bridgecount
if storage.forcedata[force].energy == nil then
storage.forcedata[force].energy = 0
end
if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then
for i, bridge in pairs(bridges) do
if validate_energy_link_bridge(i, bridge) then
energy_increment = get_energy_increment(bridge)
if bridge.energy > energy_increment*3 then
storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY)
bridge.energy = bridge.energy - energy_increment
end
for i, bridge in ipairs(bridges) do
if bridge.energy > ENERGY_INCREMENT*3 then
storage.forcedata[force].energy = storage.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY)
bridge.energy = bridge.energy - ENERGY_INCREMENT
end
end
end
for i, bridge in pairs(bridges) do
if validate_energy_link_bridge(i, bridge) then
energy_increment = get_energy_increment(bridge)
if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then
break
end
if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then
storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment
bridge.energy = bridge.energy + energy_increment
end
for i, bridge in ipairs(bridges) do
if storage.forcedata[force].energy < ENERGY_INCREMENT then
break
end
if bridge.energy < ENERGY_INCREMENT*2 and storage.forcedata[force].energy > ENERGY_INCREMENT then
storage.forcedata[force].energy = storage.forcedata[force].energy - ENERGY_INCREMENT
bridge.energy = bridge.energy + ENERGY_INCREMENT
end
end
end
end
function string_starts_with(str, start)
return str:sub(1, #start) == start
end
function validate_energy_link_bridge(unit_number, entity)
if not entity then
if storage.energy_link_bridges[unit_number] == nil then return false end
storage.energy_link_bridges[unit_number] = nil
return false
end
if not entity.valid then
if storage.energy_link_bridges[unit_number] == nil then return false end
storage.energy_link_bridges[unit_number] = nil
return false
end
return true
end
function on_energy_bridge_constructed(entity)
if entity and entity.valid then
if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
storage.energy_link_bridges[entity.unit_number] = entity
end
end
end
function on_energy_bridge_removed(entity)
if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
if storage.energy_link_bridges[entity.unit_number] == nil then return end
storage.energy_link_bridges[entity.unit_number] = nil
end
end
if (ENERGY_INCREMENT) then
script.on_event(defines.events.on_tick, on_check_energy_link)
script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end)
script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end)
script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end)
script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
end
{% if not imported_blueprints -%}
@@ -466,7 +410,6 @@ script.on_init(function()
{% if not imported_blueprints %}set_permissions(){% endif %}
storage.forcedata = {}
storage.playerdata = {}
storage.energy_link_bridges = {}
-- Fire dummy events for all currently existing forces.
local e = {}
for name, _ in pairs(game.forces) do

View File

@@ -69,7 +69,7 @@ def locality_rules(multiworld: MultiWorld):
if (location.player, location.item_rule) in func_cache:
location.item_rule = func_cache[location.player, location.item_rule]
# empty rule that just returns True, overwrite
elif location.item_rule is Location.item_rule:
elif location.item_rule is location.__class__.item_rule:
func_cache[location.player, location.item_rule] = location.item_rule = \
lambda i, sending_blockers = forbid_data[location.player], \
old_rule = location.item_rule: \
@@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"):
old_rule = spot.access_rule
# empty rule, replace instead of add
if old_rule is Location.access_rule or old_rule is Entrance.access_rule:
if old_rule is spot.__class__.access_rule:
spot.access_rule = rule if combine == "and" else old_rule
else:
if combine == "and":
@@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
def forbid_item(location: "BaseClasses.Location", item: str, player: int):
old_rule = location.item_rule
# empty rule
if old_rule is Location.item_rule:
if old_rule is location.__class__.item_rule:
location.item_rule = lambda i: i.name != item or i.player != player
else:
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
@@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"):
old_rule = location.item_rule
# empty rule, replace instead of add
if old_rule is Location.item_rule:
if old_rule is location.__class__.item_rule:
location.item_rule = rule if combine == "and" else old_rule
else:
if combine == "and":

View File

@@ -235,11 +235,6 @@ def set_rules(kh1world):
lambda state: (
state.has("Progressive Glide", player)
or
(
state.has("High Jump", player, 2)
and state.has("Footprints", player)
)
or
(
options.advanced_logic
and state.has_all({
@@ -251,11 +246,6 @@ def set_rules(kh1world):
lambda state: (
state.has("Progressive Glide", player)
or
(
state.has("High Jump", player, 2)
and state.has("Footprints", player)
)
or
(
options.advanced_logic
and state.has_all({
@@ -268,6 +258,7 @@ def set_rules(kh1world):
state.has("Footprints", player)
or (options.advanced_logic and state.has("Progressive Glide", player))
or state.has("High Jump", player, 2)
))
add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"),
lambda state: (
@@ -385,7 +376,7 @@ def set_rules(kh1world):
lambda state: state.has("White Trinity", player))
add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"),
lambda state: (
state.has_all(("High Jump", "Progressive Glide"), player)
state.has("High Jump", player)
or (options.advanced_logic and state.has("Combo Master", player))
))
add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"),
@@ -395,7 +386,7 @@ def set_rules(kh1world):
))
add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"),
lambda state: (
state.has_all(("High Jump", "Progressive Glide"), player)
state.has("High Jump", player)
or (options.advanced_logic and state.has("Combo Master", player))
))
add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"),
@@ -604,7 +595,6 @@ def set_rules(kh1world):
lambda state: (
state.has("Green Trinity", player)
and has_all_magic_lvx(state, player, 2)
and has_defensive_tools(state, player)
))
add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"),
lambda state: (
@@ -720,7 +710,8 @@ def set_rules(kh1world):
lambda state: state.has("White Trinity", player))
add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"),
lambda state: (
state.has("Progressive Glide", player)
state.has("High Jump", player)
or state.has("Progressive Glide", player)
))
add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"),
lambda state: (
@@ -1450,11 +1441,10 @@ def set_rules(kh1world):
has_emblems(state, player, options.keyblades_unlock_chests)
and has_x_worlds(state, player, 7, options.keyblades_unlock_chests)
and has_defensive_tools(state, player)
and state.has("Progressive Blizzard", player, 3)
))
add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"),
lambda state: (
has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3)
has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player)
))
if options.super_bosses or options.goal.current_key == "sephiroth":
add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"),

View File

@@ -96,13 +96,13 @@ class MM2World(World):
location_name_groups = location_groups
web = MM2WebWorld()
rom_name: bytearray
world_version: Tuple[int, int, int] = (0, 3, 2)
world_version: Tuple[int, int, int] = (0, 3, 1)
wily_5_weapons: Dict[int, List[int]]
def __init__(self, multiworld: MultiWorld, player: int):
def __init__(self, world: MultiWorld, player: int):
self.rom_name = bytearray()
self.rom_name_available_event = threading.Event()
super().__init__(multiworld, player)
super().__init__(world, player)
self.weapon_damage = deepcopy(weapon_damage)
self.wily_5_weapons = {}

View File

@@ -133,6 +133,28 @@ def set_rules(world: "MM2World") -> None:
# Wily Machine needs all three weaknesses present, so allow
elif 4 > world.weapon_damage[weapon][i] > 0:
world.weapon_damage[weapon][i] = 0
# handle special cases
for boss in range(14):
for weapon in (1, 3, 6, 8):
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)):
# Weapon does not have enough possible ammo to kill the boss, raise the damage
if boss == 9:
if weapon != 3:
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
elif boss == 11:
if weapon == 1:
# Atomic Fire cannot be Boobeam Trap's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
else:
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
starting = world.options.starting_robot_master.value
world.weapon_damage[0][starting] = 1
for p_boss in world.options.plando_weakness:
for p_weapon in world.options.plando_weakness[p_boss]:
@@ -146,28 +168,6 @@ def set_rules(world: "MM2World") -> None:
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
= world.options.plando_weakness[p_boss][p_weapon]
# handle special cases
for boss in range(14):
for weapon in (1, 2, 3, 6, 8):
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
for i in range(9) if i != weapon)):
# Weapon does not have enough possible ammo to kill the boss, raise the damage
if boss == 9:
if weapon in (1, 6):
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
elif boss == 11:
if weapon == 1:
# Atomic Fire cannot be Boobeam Trap's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
else:
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
@@ -209,11 +209,11 @@ def set_rules(world: "MM2World") -> None:
continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp]
used_weapons[boss].add(wp)
if int(uses * boss_damage[wp]) > boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0
used_weapons[boss].add(wp)
elif highest <= 0:
# we are out of weapons that can actually damage the boss
# so find the weapon that has the most uses, and apply that as an additional weakness
@@ -221,21 +221,18 @@ def set_rules(world: "MM2World") -> None:
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
# be able to cover
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
if weapon != 0 and (weapon != 8 or boss != 12))
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this
if weapon != 0)
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
ceil(boss_health[boss] / minimum_weakness_requirement[wp]))
ceil(boss_health[boss] // minimum_weakness_requirement[wp]))
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
weapon_weight.pop(wp)
used_weapons[boss].add(wp)
else:
# drain the weapon and continue
boss_health[boss] -= int(uses * boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * uses
weapon_weight.pop(wp)
used_weapons[boss].add(wp)
world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons}

View File

@@ -1,7 +1,7 @@
from typing import DefaultDict
from collections import defaultdict
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, {
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, {
' ': 0x40,
'A': 0x41,
'B': 0x42,

View File

@@ -57,11 +57,11 @@ location_rows = [
LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12),
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2),
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0),
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0),
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0),
LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0),
LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),

View File

@@ -31,7 +31,7 @@ class RegionNames(str, Enum):
Mudskipper_Point = "Mudskipper Point"
Karamja = "Karamja"
Corsair_Cove = "Corsair Cove"
Wilderness = "Wilderness"
Wilderness = "The Wilderness"
Crandor = "Crandor"
# Resource Regions
Egg = "Egg"

View File

@@ -1,337 +0,0 @@
"""
Ensures a target level can be reached with available resources
"""
from worlds.generic.Rules import CollectionRule, add_rule
from .Names import RegionNames, ItemNames
def get_fishing_skill_rule(level, player, options) -> CollectionRule:
if options.max_fishing_level < level:
return lambda state: False
if options.brutal_grinds or level < 5:
return lambda state: state.can_reach_region(RegionNames.Shrimp, player)
if level < 20:
return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \
state.can_reach_region(RegionNames.Port_Sarim, player)
else:
return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \
state.can_reach_region(RegionNames.Port_Sarim, player) and \
state.can_reach_region(RegionNames.Fly_Fish, player)
def get_mining_skill_rule(level, player, options) -> CollectionRule:
if options.max_mining_level < level:
return lambda state: False
if options.brutal_grinds or level < 15:
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \
state.can_reach_region(RegionNames.Clay_Rock, player)
else:
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or
state.can_reach_region(RegionNames.Clay_Rock, player)) and \
state.can_reach_region(RegionNames.Iron_Rock, player)
def get_woodcutting_skill_rule(level, player, options) -> CollectionRule:
if options.max_woodcutting_level < level:
return lambda state: False
if options.brutal_grinds or level < 15:
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
# Even the desert.
return lambda state: True
if level < 30:
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player)
else:
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \
state.can_reach_region(RegionNames.Willow_Tree, player)
def get_smithing_skill_rule(level, player, options) -> CollectionRule:
if options.max_smithing_level < level:
return lambda state: False
if options.brutal_grinds:
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Furnace, player)
if level < 15:
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
# in the "Anvil" resource region. We still need to check for it though.
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Furnace, player) and \
(state.can_reach_region(RegionNames.Anvil, player) or
state.can_reach_region(RegionNames.Lumbridge, player))
if level < 30:
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Iron_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and \
state.can_reach_region(RegionNames.Anvil, player)
else:
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
state.can_reach_region(RegionNames.Iron_Rock, player) and \
state.can_reach_region(RegionNames.Coal_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and \
state.can_reach_region(RegionNames.Anvil, player)
def get_crafting_skill_rule(level, player, options):
if options.max_crafting_level < level:
return lambda state: False
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
def can_spin(state):
return state.can_reach_region(RegionNames.Sheep, player) and \
state.can_reach_region(RegionNames.Spinning_Wheel, player)
def can_pot(state):
return state.can_reach_region(RegionNames.Clay_Rock, player) and \
state.can_reach_region(RegionNames.Barbarian_Village, player)
def can_tan(state):
return state.can_reach_region(RegionNames.Milk, player) and \
state.can_reach_region(RegionNames.Al_Kharid, player)
def mould_access(state):
return state.can_reach_region(RegionNames.Al_Kharid, player) or \
state.can_reach_region(RegionNames.Rimmington, player)
def can_silver(state):
return state.can_reach_region(RegionNames.Silver_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and mould_access(state)
def can_gold(state):
return state.can_reach_region(RegionNames.Gold_Rock, player) and \
state.can_reach_region(RegionNames.Furnace, player) and mould_access(state)
if options.brutal_grinds or level < 5:
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
can_smelt_gold = get_smithing_skill_rule(40, player, options)
can_smelt_silver = get_smithing_skill_rule(20, player, options)
if level < 16:
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
else:
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
(can_gold(state) and can_smelt_gold(state))
def get_cooking_skill_rule(level, player, options) -> CollectionRule:
if options.max_cooking_level < level:
return lambda state: False
if options.brutal_grinds or level < 15:
return lambda state: state.can_reach_region(RegionNames.Milk, player) or \
state.can_reach_region(RegionNames.Egg, player) or \
state.can_reach_region(RegionNames.Shrimp, player) or \
(state.can_reach_region(RegionNames.Wheat, player) and
state.can_reach_region(RegionNames.Windmill, player))
else:
can_catch_fly_fish = get_fishing_skill_rule(20, player, options)
return lambda state: (
(state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or
(state.can_reach_region(RegionNames.Port_Sarim, player))
) and (
state.can_reach_region(RegionNames.Milk, player) or
state.can_reach_region(RegionNames.Egg, player) or
state.can_reach_region(RegionNames.Shrimp, player) or
(state.can_reach_region(RegionNames.Wheat, player) and
state.can_reach_region(RegionNames.Windmill, player))
)
def get_runecraft_skill_rule(level, player, options) -> CollectionRule:
if options.max_runecraft_level < level:
return lambda state: False
if not options.brutal_grinds:
# Ensure access to the relevant altars
if level >= 5:
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player) and \
state.can_reach_region(RegionNames.Lumbridge_Swamp, player)
if level >= 9:
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player) and \
state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \
state.can_reach_region(RegionNames.East_Of_Varrock, player)
if level >= 14:
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player) and \
state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \
state.can_reach_region(RegionNames.East_Of_Varrock, player) and \
state.can_reach_region(RegionNames.Al_Kharid, player)
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
state.can_reach_region(RegionNames.Falador_Farm, player)
def get_magic_skill_rule(level, player, options) -> CollectionRule:
if options.max_magic_level < level:
return lambda state: False
return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player)
def get_firemaking_skill_rule(level, player, options) -> CollectionRule:
if options.max_firemaking_level < level:
return lambda state: False
if not options.brutal_grinds:
if level >= 30:
can_chop_willows = get_woodcutting_skill_rule(30, player, options)
return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state)
if level >= 15:
can_chop_oaks = get_woodcutting_skill_rule(15, player, options)
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state)
# If brutal grinds are on, or if the level is less than 15, you can train it.
return lambda state: True
def get_skill_rule(skill, level, player, options) -> CollectionRule:
if skill.lower() == "fishing":
return get_fishing_skill_rule(level, player, options)
if skill.lower() == "mining":
return get_mining_skill_rule(level, player, options)
if skill.lower() == "woodcutting":
return get_woodcutting_skill_rule(level, player, options)
if skill.lower() == "smithing":
return get_smithing_skill_rule(level, player, options)
if skill.lower() == "crafting":
return get_crafting_skill_rule(level, player, options)
if skill.lower() == "cooking":
return get_cooking_skill_rule(level, player, options)
if skill.lower() == "runecraft":
return get_runecraft_skill_rule(level, player, options)
if skill.lower() == "magic":
return get_magic_skill_rule(level, player, options)
if skill.lower() == "firemaking":
return get_firemaking_skill_rule(level, player, options)
return lambda state: True
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options):
if outbound_region_name == RegionNames.Cooks_Guild:
add_rule(entrance, get_cooking_skill_rule(32, player, options))
elif outbound_region_name == RegionNames.Crafting_Guild:
add_rule(entrance, get_crafting_skill_rule(40, player, options))
elif outbound_region_name == RegionNames.Corsair_Cove:
# Need to be able to start Corsair Curse in addition to having the item
add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player))
elif outbound_region_name == "Camdozaal*":
add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player))
elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
# Special logic for canoes
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
RegionNames.Edgeville, RegionNames.Wilderness]
if region_row.name in canoe_regions:
# Skill rules for greater distances
woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options)
woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options)
woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options)
woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options)
if region_row.name == RegionNames.Lumbridge:
# Canoe Tree access for the Location
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d3(state)) or
(state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_all(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d1)
elif outbound_region_name == RegionNames.Barbarian_Village:
add_rule(entrance, woodcutting_rule_d2)
elif outbound_region_name == RegionNames.Edgeville:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.Wilderness:
add_rule(entrance, woodcutting_rule_all)
elif region_row.name == RegionNames.South_Of_Varrock:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_d3(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_d1)
elif outbound_region_name == RegionNames.Barbarian_Village:
add_rule(entrance, woodcutting_rule_d1)
elif outbound_region_name == RegionNames.Edgeville:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.Wilderness:
add_rule(entrance, woodcutting_rule_all)
elif region_row.name == RegionNames.Barbarian_Village:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_d2(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_d2)
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d1)
# Edgeville does not need to be checked, because it's already adjacent
elif outbound_region_name == RegionNames.Wilderness:
add_rule(entrance, woodcutting_rule_d3)
elif region_row.name == RegionNames.Edgeville:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_d3(state)) or
(state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d1(state)) or
(state.can_reach_region(RegionNames.Wilderness, player)
and woodcutting_rule_d1(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d2)
# Barbarian Village does not need to be checked, because it's already adjacent
# Wilderness does not need to be checked, because it's already adjacent
elif region_row.name == RegionNames.Wilderness:
if outbound_region_name == RegionNames.Canoe_Tree:
add_rule(entrance,
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
and woodcutting_rule_all(state)) or
(state.can_reach_region(RegionNames.South_Of_Varrock, player)
and woodcutting_rule_d3(state)) or
(state.can_reach_region(RegionNames.Barbarian_Village, player)
and woodcutting_rule_d2(state)) or
(state.can_reach_region(RegionNames.Edgeville, player)
and woodcutting_rule_d1(state)))
# Access to other chunks based on woodcutting settings
elif outbound_region_name == RegionNames.Lumbridge:
add_rule(entrance, woodcutting_rule_all)
elif outbound_region_name == RegionNames.South_Of_Varrock:
add_rule(entrance, woodcutting_rule_d3)
elif outbound_region_name == RegionNames.Barbarian_Village:
add_rule(entrance, woodcutting_rule_d2)
# Edgeville does not need to be checked, because it's already adjacent

View File

@@ -1,12 +1,12 @@
import typing
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState
from Fill import fill_restrictive, FillError
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_rule, CollectionRule
from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
chunksanity_special_region_names
from .Locations import OSRSLocation, LocationRow
from .Rules import *
from .Options import OSRSOptions, StartingArea
from .Names import LocationNames, ItemNames, RegionNames
@@ -46,7 +46,6 @@ class OSRSWorld(World):
web = OSRSWeb()
base_id = 0x070000
data_version = 1
explicit_indirect_conditions = False
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))}
location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))}
@@ -62,7 +61,6 @@ class OSRSWorld(World):
starting_area_item: str
locations_by_category: typing.Dict[str, typing.List[LocationRow]]
available_QP_locations: typing.List[str]
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
@@ -77,7 +75,6 @@ class OSRSWorld(World):
self.starting_area_item = ""
self.locations_by_category = {}
self.available_QP_locations = []
def generate_early(self) -> None:
location_categories = [location_row.category for location_row in location_rows]
@@ -93,9 +90,9 @@ class OSRSWorld(World):
rnd = self.random
starting_area = self.options.starting_area
#UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT
if not hasattr(self.multiworld, "generation_is_fake"):
if not hasattr(self.multiworld, "generation_is_fake"):
if starting_area.value == StartingArea.option_any_bank:
self.starting_area_item = rnd.choice(starting_area_dict)
elif starting_area.value < StartingArea.option_chunksanity:
@@ -130,6 +127,7 @@ class OSRSWorld(World):
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
def create_regions(self) -> None:
"""
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
@@ -147,8 +145,7 @@ class OSRSWorld(World):
# Removes the word "Area: " from the item name to get the region it applies to.
# I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse
# if area hasn't been set, then we shouldn't connect it
if self.starting_area_item != "":
if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
@@ -167,8 +164,11 @@ class OSRSWorld(World):
entrance.connect(self.region_name_to_data[parsed_outbound])
item_name = self.region_rows_by_name[parsed_outbound].itemReq
entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player)
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options)
if "*" not in outbound_region_name and "*" not in item_name:
entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player)
continue
self.generate_special_rules_for(entrance, region_row, outbound_region_name)
for resource_region in region_row.resources:
if not resource_region:
@@ -178,34 +178,321 @@ class OSRSWorld(World):
if "*" not in resource_region:
entrance.connect(self.region_name_to_data[resource_region])
else:
self.generate_special_rules_for(entrance, region_row, resource_region)
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options)
self.roll_locations()
def task_within_skill_levels(self, skills_required):
# Loop through each required skill. If any of its requirements are out of the defined limit, return false
for skill in skills_required:
max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level")
if skill.level > max_level_for_skill:
return False
return True
def generate_special_rules_for(self, entrance, region_row, outbound_region_name):
# print(f"Special rules required to access region {outbound_region_name} from {region_row.name}")
if outbound_region_name == RegionNames.Cooks_Guild:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
cooking_level_rule = self.get_skill_rule("cooking", 32)
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
cooking_level_rule(state)
if self.options.brutal_grinds:
cooking_level_32_regions = {
RegionNames.Milk,
RegionNames.Egg,
RegionNames.Shrimp,
RegionNames.Wheat,
RegionNames.Windmill,
}
else:
# Level 15 cooking and higher requires level 20 fishing.
fishing_level_20_regions = {
RegionNames.Shrimp,
RegionNames.Port_Sarim,
}
cooking_level_32_regions = {
RegionNames.Milk,
RegionNames.Egg,
RegionNames.Shrimp,
RegionNames.Wheat,
RegionNames.Windmill,
RegionNames.Fly_Fish,
*fishing_level_20_regions,
}
for region_name in cooking_level_32_regions:
self.multiworld.register_indirect_condition(self.get_region(region_name), entrance)
return
if outbound_region_name == RegionNames.Crafting_Guild:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
crafting_level_rule = self.get_skill_rule("crafting", 40)
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
crafting_level_rule(state)
if self.options.brutal_grinds:
crafting_level_40_regions = {
# can_spin
RegionNames.Sheep,
RegionNames.Spinning_Wheel,
# can_pot
RegionNames.Clay_Rock,
RegionNames.Barbarian_Village,
# can_tan
RegionNames.Milk,
RegionNames.Al_Kharid,
}
else:
mould_access_regions = {
RegionNames.Al_Kharid,
RegionNames.Rimmington,
}
smithing_level_20_regions = {
RegionNames.Bronze_Ores,
RegionNames.Iron_Rock,
RegionNames.Furnace,
RegionNames.Anvil,
}
smithing_level_40_regions = {
*smithing_level_20_regions,
RegionNames.Coal_Rock,
}
crafting_level_40_regions = {
# can_tan
RegionNames.Milk,
RegionNames.Al_Kharid,
# can_silver
RegionNames.Silver_Rock,
RegionNames.Furnace,
*mould_access_regions,
# can_smelt_silver
*smithing_level_20_regions,
# can_gold
RegionNames.Gold_Rock,
RegionNames.Furnace,
*mould_access_regions,
# can_smelt_gold
*smithing_level_40_regions,
}
for region_name in crafting_level_40_regions:
self.multiworld.register_indirect_condition(self.get_region(region_name), entrance)
return
if outbound_region_name == RegionNames.Corsair_Cove:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
# Need to be able to start Corsair Curse in addition to having the item
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
state.can_reach(RegionNames.Falador_Farm, "Region", self.player)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance)
return
if outbound_region_name == "Camdozaal*":
item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
state.has(ItemNames.QP_Below_Ice_Mountain, self.player)
return
if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player)
return
# Special logic for canoes
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
RegionNames.Edgeville, RegionNames.Wilderness]
if region_row.name in canoe_regions:
# Skill rules for greater distances
woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12)
woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27)
woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42)
woodcutting_rule_all = self.get_skill_rule("woodcutting", 57)
def add_indirect_conditions_for_woodcutting_levels(entrance, *levels: int):
if self.options.brutal_grinds:
# No access to specific regions required.
return
# Currently, each level requirement requires everything from the previous level requirements, so the
# maximum level requirement can be taken.
max_level = max(levels, default=0)
max_level = min(max_level, self.options.max_woodcutting_level.value)
if 15 <= max_level < 30:
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance)
elif 30 <= max_level:
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance)
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Willow_Tree), entrance)
if region_row.name == RegionNames.Lumbridge:
# Canoe Tree access for the Location
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
# South of Varrock does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
if outbound_region_name == RegionNames.Edgeville:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 57
add_indirect_conditions_for_woodcutting_levels(entrance, 57)
if region_row.name == RegionNames.South_Of_Varrock:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
# Lumbridge does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
and self.options.max_woodcutting_level >= 12
add_indirect_conditions_for_woodcutting_levels(entrance, 12)
if outbound_region_name == RegionNames.Edgeville:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if region_row.name == RegionNames.Barbarian_Village:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
and self.options.max_woodcutting_level >= 12
add_indirect_conditions_for_woodcutting_levels(entrance, 12)
# Edgeville does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if region_row.name == RegionNames.Edgeville:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
# Barbarian Village does not need to be checked, because it's already adjacent
# Wilderness does not need to be checked, because it's already adjacent
if region_row.name == RegionNames.Wilderness:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 57
add_indirect_conditions_for_woodcutting_levels(entrance, 57)
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
# Edgeville does not need to be checked, because it's already adjacent
def roll_locations(self):
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
locations_required = 0
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
for item_row in item_rows:
locations_required += item_row.amount
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
# Quests are always added first, before anything else is rolled
# Quests are always added
for i, location_row in enumerate(location_rows):
if location_row.category in {"quest", "points", "goal"}:
if self.task_within_skill_levels(location_row.skills):
self.create_and_add_location(i)
if location_row.category == "quest":
locations_added += 1
self.create_and_add_location(i)
if location_row.category == "quest":
locations_added += 1
# Build up the weighted Task Pool
rnd = self.random
@@ -229,9 +516,10 @@ class OSRSWorld(World):
task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
for task_type in task_types:
max_level_for_task_type = getattr(self.options, f"max_{task_type}_level")
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
if self.task_within_skill_levels(task.skills)]
if task.skills[0].level <= max_level_for_task_type]
if not self.options.progressive_tasks:
rnd.shuffle(tasks_for_this_type)
else:
@@ -280,7 +568,6 @@ class OSRSWorld(World):
self.add_location(task)
locations_added += 1
def add_location(self, location):
index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0]
self.create_and_add_location(index)
@@ -299,15 +586,11 @@ class OSRSWorld(World):
def create_and_add_location(self, row_index) -> None:
location_row = location_rows[row_index]
# Quest Points are handled differently now, but in case this gets fed an older version of the data sheet,
# the points might still be listed in a different row
if location_row.category == "points":
return
# print(f"Adding task {location_row.name}")
# Create Location
location_id = self.base_id + row_index
if location_row.category == "goal":
if location_row.category == "points" or location_row.category == "goal":
location_id = None
location = OSRSLocation(self.player, location_row.name, location_id)
self.location_name_to_data[location_row.name] = location
@@ -319,14 +602,6 @@ class OSRSWorld(World):
location.parent_region = region
region.locations.append(location)
# If it's a quest, generate a "Points" location we'll add an event to
if location_row.category == "quest":
points_name = location_row.name.replace("Quest:", "Points:")
points_location = OSRSLocation(self.player, points_name)
self.location_name_to_data[points_name] = points_location
points_location.parent_region = region
region.locations.append(points_location)
def set_rules(self) -> None:
"""
called to set access and item rules on locations and entrances.
@@ -337,26 +612,18 @@ class OSRSWorld(World):
"Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure",
"Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot",
"Below_Ice_Mountain"]
for qp_attr_name in quest_attr_names:
loc_name = getattr(LocationNames, f"QP_{qp_attr_name}")
item_name = getattr(ItemNames, f"QP_{qp_attr_name}")
self.multiworld.get_location(loc_name, self.player) \
.place_locked_item(self.create_event(item_name))
for quest_attr_name in quest_attr_names:
qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}")
qp_loc = self.location_name_to_data.get(qp_loc_name)
q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}")
q_loc = self.location_name_to_data.get(q_loc_name)
# Checks to make sure the task is actually in the list before trying to create its rules
if qp_loc and q_loc:
# Create the QP Event Item
item_name = getattr(ItemNames, f"QP_{quest_attr_name}")
qp_loc.place_locked_item(self.create_event(item_name))
# If a quest is excluded, don't actually consider it for quest point progression
if q_loc_name not in self.options.exclude_locations:
self.available_QP_locations.append(item_name)
# Set the access rule for the QP Location
add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state)))
add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: (
self.multiworld.get_location(q_loc_name, self.player).can_reach(state)
))
# place "Victory" at "Dragon Slayer" and set collection as win condition
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \
@@ -372,7 +639,7 @@ class OSRSWorld(World):
lambda state, region_required=region_required: state.can_reach(region_required, "Region",
self.player))
for skill_req in location_row.skills:
add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options))
add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level))
for item_req in location_row.items:
add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player))
if location_row.qp:
@@ -397,8 +664,124 @@ class OSRSWorld(World):
def quest_points(self, state):
qp = 0
for qp_event in self.available_QP_locations:
for qp_event in QP_Items:
if state.has(qp_event, self.player):
qp += int(qp_event[0])
return qp
"""
Ensures a target level can be reached with available resources
"""
def get_skill_rule(self, skill, level) -> CollectionRule:
if skill.lower() == "fishing":
if self.options.brutal_grinds or level < 5:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player)
if level < 20:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
state.can_reach(RegionNames.Port_Sarim, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \
state.can_reach(RegionNames.Fly_Fish, "Region", self.player)
if skill.lower() == "mining":
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)
else:
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player)
if skill.lower() == "woodcutting":
if self.options.brutal_grinds or level < 15:
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
# Even the desert.
return lambda state: True
if level < 30:
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \
state.can_reach(RegionNames.Willow_Tree, "Region", self.player)
if skill.lower() == "smithing":
if self.options.brutal_grinds:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player)
if level < 15:
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
# in the "Anvil" resource region. We still need to check for it though.
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
(state.can_reach(RegionNames.Anvil, "Region", self.player) or
state.can_reach(RegionNames.Lumbridge, "Region", self.player))
if level < 30:
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
state.can_reach(RegionNames.Anvil, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
state.can_reach(RegionNames.Anvil, "Region", self.player)
if skill.lower() == "crafting":
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
def can_spin(state):
return state.can_reach(RegionNames.Sheep, "Region", self.player) and \
state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player)
def can_pot(state):
return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Barbarian_Village, "Region", self.player)
def can_tan(state):
return state.can_reach(RegionNames.Milk, "Region", self.player) and \
state.can_reach(RegionNames.Al_Kharid, "Region", self.player)
def mould_access(state):
return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \
state.can_reach(RegionNames.Rimmington, "Region", self.player)
def can_silver(state):
return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
def can_gold(state):
return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
if self.options.brutal_grinds or level < 5:
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
can_smelt_gold = self.get_skill_rule("smithing", 40)
can_smelt_silver = self.get_skill_rule("smithing", 20)
if level < 16:
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
else:
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
(can_gold(state) and can_smelt_gold(state))
if skill.lower() == "cooking":
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \
state.can_reach(RegionNames.Egg, "Region", self.player) or \
state.can_reach(RegionNames.Shrimp, "Region", self.player) or \
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
state.can_reach(RegionNames.Windmill, "Region", self.player))
else:
can_catch_fly_fish = self.get_skill_rule("fishing", 20)
return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \
can_catch_fly_fish(state) and \
(state.can_reach(RegionNames.Milk, "Region", self.player) or
state.can_reach(RegionNames.Egg, "Region", self.player) or
state.can_reach(RegionNames.Shrimp, "Region", self.player) or
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
state.can_reach(RegionNames.Windmill, "Region", self.player)))
if skill.lower() == "runecraft":
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player)
if skill.lower() == "magic":
return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player)
return lambda state: True

View File

@@ -41,7 +41,9 @@ all_random_settings = {
Friendsanity.internal_name: "random",
FriendsanityHeartSize.internal_name: "random",
Booksanity.internal_name: "random",
Walnutsanity.internal_name: "random",
NumberOfMovementBuffs.internal_name: "random",
EnabledFillerBuffs.internal_name: "random",
ExcludeGingerIsland.internal_name: "random",
TrapItems.internal_name: "random",
MultipleDaySleepEnabled.internal_name: "random",

View File

@@ -112,7 +112,8 @@ class AggressiveScanLogic(Choice):
class SubnauticaDeathLink(DeathLink):
__doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"."
"""When you die, everyone dies. Of course the reverse is true too.
Note: can be toggled via in-game console command "deathlink"."""
class FillerItemsDistribution(ItemDict):

View File

@@ -379,7 +379,6 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
cantoran: Cantoran
lore_checks: LoreChecks
boss_rando: BossRando
enemy_rando: EnemyRando
damage_rando: DamageRando
damage_rando_overrides: DamageRandoOverrides
hp_cap: HpCap
@@ -446,7 +445,6 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
Cantoran: hidden(Cantoran) # type: ignore
LoreChecks: hidden(LoreChecks) # type: ignore
BossRando: hidden(BossRando) # type: ignore
EnemyRando: hidden(EnemyRando) # type: ignore
DamageRando: hidden(DamageRando) # type: ignore
DamageRandoOverrides: HiddenDamageRandoOverrides
HpCap: hidden(HpCap) # type: ignore
@@ -518,10 +516,6 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
self.boss_rando == BossRando.default:
self.boss_rando.value = self.BossRando.value
self.has_replaced_options.value = Toggle.option_true
if self.EnemyRando != EnemyRando.default and \
self.enemy_rando == EnemyRando.default:
self.enemy_rando.value = self.EnemyRando.value
self.has_replaced_options.value = Toggle.option_true
if self.DamageRando != DamageRando.default and \
self.damage_rando == DamageRando.default:
self.damage_rando.value = self.DamageRando.value

View File

@@ -98,7 +98,6 @@ class TimespinnerWorld(World):
"Cantoran": self.options.cantoran.value,
"LoreChecks": self.options.lore_checks.value,
"BossRando": self.options.boss_rando.value,
"EnemyRando": self.options.enemy_rando.value,
"DamageRando": self.options.damage_rando.value,
"DamageRandoOverrides": self.options.damage_rando_overrides.value,
"HpCap": self.options.hp_cap.value,

View File

@@ -108,15 +108,11 @@ sword_cave_locations = [
]
food_locations = [
"Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
"Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
"Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)",
"Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)"
]
gohma_locations = [
"Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)"
]
gleeok_locations = [
"Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce"
]

View File

@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from worlds.generic.Rules import add_rule
from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations
from .Locations import food_locations, shop_locations, gleeok_locations
from .ItemPool import dangerous_weapon_locations
from .Options import StartingPosition
@@ -10,12 +10,13 @@ if TYPE_CHECKING:
def set_rules(tloz_world: "TLoZWorld"):
player = tloz_world.player
world = tloz_world.multiworld
options = tloz_world.options
# Boss events for a nicer spoiler log play through
for level in range(1, 9):
boss = tloz_world.get_location(f"Level {level} Boss")
boss_event = tloz_world.get_location(f"Level {level} Boss Status")
boss = world.get_location(f"Level {level} Boss", player)
boss_event = world.get_location(f"Level {level} Boss Status", player)
status = tloz_world.create_event(f"Boss {level} Defeated")
boss_event.place_locked_item(status)
add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player))
@@ -25,131 +26,136 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in level.locations:
if options.StartingPosition < StartingPosition.option_dangerous \
or location.name not in dangerous_weapon_locations:
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("weapons", player))
# This part of the loop sets up an expected amount of defense needed for each dungeon
if i > 0: # Don't need an extra heart for Level 1
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location(location.name, player),
lambda state, hearts=i: state.has("Heart Container", player, hearts) or
(state.has("Blue Ring", player) and
state.has("Heart Container", player, int(hearts / 2))) or
(state.has("Red Ring", player) and
state.has("Heart Container", player, int(hearts / 4))))
if "Pols Voice" in location.name: # This enemy needs specific weapons
add_rule(tloz_world.get_location(location.name),
lambda state: state.has_group("swords", player) or
(state.has("Bow", player) and state.has_group("arrows", player)))
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("swords", player) or state.has("Bow", player))
# No requiring anything in a shop until we can farm for money
for location in shop_locations:
add_rule(tloz_world.get_location(location),
add_rule(world.get_location(location, player),
lambda state: state.has_group("weapons", player))
# Everything from 4 on up has dark rooms
for level in tloz_world.levels[4:]:
for location in level.locations:
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("candles", player)
or (state.has("Magical Rod", player) and state.has("Book of Magic", player)))
# Everything from 5 on up has gaps
for level in tloz_world.levels[5:]:
for location in level.locations:
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location(location.name, player),
lambda state: state.has("Stepladder", player))
# Level 4 Access
for location in tloz_world.levels[4].locations:
add_rule(tloz_world.get_location(location.name),
lambda state: state.has_any(("Raft", "Recorder"), player))
# Digdogger boss. Rework this once ER happens
add_rule(tloz_world.get_location("Level 5 Boss"),
lambda state: state.has("Recorder", player))
add_rule(tloz_world.get_location("Level 5 Triforce"),
add_rule(world.get_location("Level 5 Boss", player),
lambda state: state.has("Recorder", player))
for location in gohma_locations:
if options.ExpandedPool or "Drop" not in location:
add_rule(tloz_world.get_location(location),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
add_rule(world.get_location("Level 6 Boss", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
# Recorder Access for Level 7
for location in tloz_world.levels[7].locations:
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location("Level 7 Item (Red Candle)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Boss", player),
lambda state: state.has("Recorder", player))
if options.ExpandedPool:
add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player),
lambda state: state.has("Recorder", player))
for location in food_locations:
if options.ExpandedPool or "Drop" not in location:
add_rule(tloz_world.get_location(location),
add_rule(world.get_location(location, player),
lambda state: state.has("Food", player))
for location in gleeok_locations:
add_rule(tloz_world.get_location(location),
add_rule(world.get_location(location, player),
lambda state: state.has_group("swords", player) or state.has("Magical Rod", player))
# Candle access for Level 8
for location in tloz_world.levels[8].locations:
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("candles", player))
add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"),
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
if options.ExpandedPool:
add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"),
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
for location in tloz_world.levels[9].locations:
add_rule(tloz_world.get_location(location.name),
add_rule(world.get_location(location.name, player),
lambda state: state.has("Triforce Fragment", player, 8) and
state.has_group("swords", player))
# Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop
for level in range(1, 9):
add_rule(tloz_world.get_location(f"Level {level} Triforce"),
add_rule(world.get_location(f"Level {level} Triforce", player),
lambda state, l=level: state.has(f"Boss {l} Defeated", player))
# Sword, raft, and ladder spots
add_rule(tloz_world.get_location("White Sword Pond"),
add_rule(world.get_location("White Sword Pond", player),
lambda state: state.has("Heart Container", player, 2))
add_rule(tloz_world.get_location("Magical Sword Grave"),
add_rule(world.get_location("Magical Sword Grave", player),
lambda state: state.has("Heart Container", player, 9))
stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"]
stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"]
for location in stepladder_locations:
add_rule(tloz_world.get_location(location),
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
if options.ExpandedPool:
for location in stepladder_locations_expanded:
add_rule(tloz_world.get_location(location),
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
# Don't allow Take Any Items until we can actually get in one
if options.ExpandedPool:
add_rule(tloz_world.get_location("Take Any Item Left"),
add_rule(world.get_location("Take Any Item Left", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))
add_rule(tloz_world.get_location("Take Any Item Middle"),
add_rule(world.get_location("Take Any Item Middle", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))
add_rule(tloz_world.get_location("Take Any Item Right"),
add_rule(world.get_location("Take Any Item Right", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))
for location in tloz_world.levels[4].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has("Raft", player) or state.has("Recorder", player))
for location in tloz_world.levels[7].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has("Recorder", player))
for location in tloz_world.levels[8].locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has("Bow", player))
add_rule(tloz_world.get_location("Potion Shop Item Left"),
add_rule(world.get_location("Potion Shop Item Left", player),
lambda state: state.has("Letter", player))
add_rule(tloz_world.get_location("Potion Shop Item Middle"),
add_rule(world.get_location("Potion Shop Item Middle", player),
lambda state: state.has("Letter", player))
add_rule(tloz_world.get_location("Potion Shop Item Right"),
add_rule(world.get_location("Potion Shop Item Right", player),
lambda state: state.has("Letter", player))
add_rule(tloz_world.get_location("Shield Shop Item Left"),
add_rule(world.get_location("Shield Shop Item Left", player),
lambda state: state.has_group("candles", player) or
state.has("Bomb", player))
add_rule(tloz_world.get_location("Shield Shop Item Middle"),
add_rule(world.get_location("Shield Shop Item Middle", player),
lambda state: state.has_group("candles", player) or
state.has("Bomb", player))
add_rule(tloz_world.get_location("Shield Shop Item Right"),
add_rule(world.get_location("Shield Shop Item Right", player),
lambda state: state.has_group("candles", player) or
state.has("Bomb", player))
state.has("Bomb", player))

View File

@@ -83,11 +83,6 @@ class TunicWorld(World):
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
# so we only loop the multiworld locations once
# if these are locations instead of their info, it gives a memory leak error
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
player_item_link_locations: Dict[str, List[Location]]
def generate_early(self) -> None:
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
self.options.laurels_zips.value = LaurelsZips.option_true
@@ -392,18 +387,6 @@ class TunicWorld(World):
if hint_text:
hint_data[self.player][location.address] = hint_text
def get_real_location(self, location: Location) -> Tuple[str, int]:
# if it's not in a group, it's not in an item link
if location.player not in self.multiworld.groups or not location.item:
return location.name, location.player
try:
loc = self.player_item_link_locations[location.item.name].pop()
return loc.name, loc.player
except IndexError:
warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. "
f"Using a potentially incorrect location name instead.")
return location.name, location.player
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {
"seed": self.random.randint(0, 2147483647),
@@ -429,35 +412,12 @@ class TunicWorld(World):
"disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race),
}
# this would be in a stage if there was an appropriate stage for it
self.player_item_link_locations = {}
groups = self.multiworld.get_player_groups(self.player)
# checking if groups so that this doesn't run if the player isn't in a group
if groups:
if not self.item_link_locations:
tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC")
# figure out our groups and the items in them
for tunic in tunic_worlds:
for group in self.multiworld.get_player_groups(tunic.player):
self.item_link_locations.setdefault(group, {})
for location in self.multiworld.get_locations():
if location.item and location.item.player in self.item_link_locations.keys():
(self.item_link_locations[location.item.player].setdefault(location.item.name, [])
.append((location.player, location.name)))
# if item links are on, set up the player's personal item link locations, so we can pop them as needed
for group, item_links in self.item_link_locations.items():
if group in groups:
for item_name, locs in item_links.items():
self.player_item_link_locations[item_name] = \
[self.multiworld.get_location(location_name, player) for player, location_name in locs]
for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items):
if tunic_item.name not in slot_data:
slot_data[tunic_item.name] = []
if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6:
continue
slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location))
slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player])
for start_item in self.options.start_inventory_from_pool:
if start_item in slot_data_item_names:
@@ -476,7 +436,7 @@ class TunicWorld(World):
if item in slot_data_item_names:
slot_data[item] = []
for item_location in self.multiworld.find_item_locations(item, self.player):
slot_data[item].extend(self.get_real_location(item_location))
slot_data[item].extend([item_location.name, item_location.player])
return slot_data

View File

@@ -807,7 +807,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
# drop a rudeling, icebolt or ice bomb
"Overworld to West Garden from Furnace":
[["IG3"], ["LS1"]],
[["IG3"]],
},
"East Overworld": {
"Above Ruined Passage":

View File

@@ -501,11 +501,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Dark Tomb Upper"].connect(
connecting_region=regions["Dark Tomb Entry Point"])
# ice grapple through the wall, get the little secret sound to trigger
regions["Dark Tomb Upper"].connect(
connecting_region=regions["Dark Tomb Main"],
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
regions["Dark Tomb Main"].connect(
connecting_region=regions["Dark Tomb Upper"],
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
@@ -781,10 +779,12 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Fortress East Shortcut Upper"].connect(
connecting_region=regions["Fortress East Shortcut Lower"])
# nmg: can ice grapple upwards
regions["Fortress East Shortcut Lower"].connect(
connecting_region=regions["Fortress East Shortcut Upper"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
# nmg: ice grapple through the big gold door, can do it both ways
regions["Eastern Vault Fortress"].connect(
connecting_region=regions["Eastern Vault Fortress Gold Door"],
rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
@@ -807,6 +807,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Fortress Hero's Grave Region"].connect(
connecting_region=regions["Fortress Grave Path"])
# nmg: ice grapple from upper grave path to lower
regions["Fortress Grave Path Upper"].connect(
connecting_region=regions["Fortress Grave Path"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
@@ -1138,9 +1139,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
for portal_dest in region_info.portals:
ls_connect(ladder_region, "Overworld Redux, " + portal_dest)
# convenient staircase means this one is easy difficulty, even though there's an elevation change
ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west")
# connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail
if options.ladder_storage >= LadderStorage.option_medium:
for ladder_region, region_info in ow_ladder_groups.items():
@@ -1156,7 +1154,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
if options.ladder_storage >= LadderStorage.option_hard:
ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_")
ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_")
ls_connect("LS Elev 2", "Overworld Redux, Ruins Passage_west")
ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house")
ls_connect("LS Elev 5", "Overworld Redux, Temple_main")

View File

@@ -17,7 +17,7 @@ ow_ladder_groups: Dict[str, OWLadderInfo] = {
["Overworld Beach"]),
# also the east filigree room
"LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"},
["Furnace_gyro_lower", "Furnace_gyro_west", "Swamp Redux 2_wall"],
["Furnace_gyro_lower", "Swamp Redux 2_wall"],
["Overworld Tunnel Turret"]),
# also the fountain filigree room and ruined passage door
"LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"},

View File

@@ -80,7 +80,7 @@ class WitnessWorld(World):
def _get_slot_data(self) -> Dict[str, Any]:
return {
"seed": self.options.puzzle_randomization_seed.value,
"seed": self.random.randrange(0, 1000000),
"victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
"panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
"item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),

View File

@@ -401,17 +401,6 @@ class DeathLinkAmnesty(Range):
default = 1
class PuzzleRandomizationSeed(Range):
"""
Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization.
This option lets you set this seed yourself.
"""
display_name = "Puzzle Randomization Seed"
range_start = 1
range_end = 9999999
default = "random"
@dataclass
class TheWitnessOptions(PerGameCommonOptions):
puzzle_randomization: PuzzleRandomization
@@ -446,7 +435,6 @@ class TheWitnessOptions(PerGameCommonOptions):
laser_hints: LaserHints
death_link: DeathLink
death_link_amnesty: DeathLinkAmnesty
puzzle_randomization_seed: PuzzleRandomizationSeed
shuffle_dog: ShuffleDog
@@ -495,7 +483,6 @@ witness_option_groups = [
ElevatorsComeToYou,
DeathLink,
DeathLinkAmnesty,
PuzzleRandomizationSeed,
]),
OptionGroup("Silly Options", [
ShuffleDog,

View File

@@ -2,7 +2,7 @@
Defines progression, junk and event items for The Witness
"""
import copy
from typing import TYPE_CHECKING, Dict, List, Set
from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld