mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-21 23:23:24 -07:00
Merge branch 'ArchipelagoMW:main' into main
This commit is contained in:
20
Fill.py
20
Fill.py
@@ -571,6 +571,26 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
print_data = {"items": items_counter, "locations": locations_counter}
|
||||
logging.info(f"Per-Player counts: {print_data})")
|
||||
|
||||
more_locations = locations_counter - items_counter
|
||||
more_items = items_counter - locations_counter
|
||||
for player in multiworld.player_ids:
|
||||
if more_locations[player]:
|
||||
logging.error(
|
||||
f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.")
|
||||
elif more_items[player]:
|
||||
logging.warning(
|
||||
f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.")
|
||||
if unfilled:
|
||||
raise FillError(
|
||||
f"Unable to fill all locations.\n" +
|
||||
f"Unfilled locations({len(unfilled)}): {unfilled}"
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Unable to place all items.\n" +
|
||||
f"Unplaced items({len(unplaced)}): {unplaced}"
|
||||
)
|
||||
|
||||
|
||||
def flood_items(multiworld: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
|
||||
@@ -743,16 +743,17 @@ class Context:
|
||||
concerns[player].append(data)
|
||||
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)
|
||||
# only remember hints that were not already found at the time of creation
|
||||
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:
|
||||
@@ -1887,7 +1888,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'Locations has to be a list of integers',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
|
||||
|
||||
12
Utils.py
12
Utils.py
@@ -940,7 +940,7 @@ def freeze_support() -> None:
|
||||
|
||||
def visualize_regions(root_region: Region, file_name: str, *,
|
||||
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
||||
linetype_ortho: bool = True) -> None:
|
||||
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None:
|
||||
"""Visualize the layout of a world as a PlantUML diagram.
|
||||
|
||||
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||
@@ -956,16 +956,22 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
Items without ID will be shown in italics.
|
||||
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
|
||||
|
||||
Example usage in World code:
|
||||
from Utils import visualize_regions
|
||||
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
|
||||
state = self.multiworld.get_all_state(False)
|
||||
state.update_reachable_regions(self.player)
|
||||
visualize_regions(self.get_region("Menu"), "my_world.puml", show_entrance_names=True,
|
||||
regions_to_highlight=state.reachable_regions[self.player])
|
||||
|
||||
Example usage in Main code:
|
||||
from Utils import visualize_regions
|
||||
for player in multiworld.player_ids:
|
||||
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
|
||||
"""
|
||||
if regions_to_highlight is None:
|
||||
regions_to_highlight = set()
|
||||
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
|
||||
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from collections import deque
|
||||
@@ -1018,7 +1024,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
|
||||
|
||||
def visualize_region(region: Region) -> None:
|
||||
uml.append(f"class \"{fmt(region)}\"")
|
||||
uml.append(f"class \"{fmt(region)}\" {'#00FF00' if region in regions_to_highlight else ''}")
|
||||
if show_locations:
|
||||
visualize_locations(region)
|
||||
visualize_exits(region)
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
# Lingo
|
||||
/worlds/lingo/ @hatkirby
|
||||
|
||||
# Links Awakening DX
|
||||
/worlds/ladx/ @threeandthreee
|
||||
|
||||
# Lufia II Ancient Cave
|
||||
/worlds/lufia2ac/ @el-u
|
||||
/worlds/lufia2ac/docs/ @wordfcuk @el-u
|
||||
@@ -236,9 +239,6 @@
|
||||
# Final Fantasy (1)
|
||||
# /worlds/ff1/
|
||||
|
||||
# Links Awakening DX
|
||||
# /worlds/ladx/
|
||||
|
||||
# Ocarina of Time
|
||||
# /worlds/oot/
|
||||
|
||||
|
||||
@@ -117,3 +117,12 @@ class TestImplemented(unittest.TestCase):
|
||||
f"\nUnexpectedly reachable locations in sphere {sphere_num}:"
|
||||
f"\n{reachable_only_with_explicit}")
|
||||
self.fail("Unreachable")
|
||||
|
||||
def test_no_items_or_locations_or_regions_submitted_in_init(self):
|
||||
"""Test that worlds don't submit items/locations/regions to the multiworld in __init__"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, ())
|
||||
self.assertEqual(len(multiworld.itempool), 0)
|
||||
self.assertEqual(len(multiworld.get_locations()), 0)
|
||||
self.assertEqual(len(multiworld.get_regions()), 0)
|
||||
|
||||
@@ -119,7 +119,9 @@ def KholdstareDefeatRule(state, player: int) -> bool:
|
||||
|
||||
|
||||
def VitreousDefeatRule(state, player: int) -> bool:
|
||||
return can_shoot_arrows(state, player) or has_melee_weapon(state, player)
|
||||
return ((can_shoot_arrows(state, player) and can_use_bombs(state, player, 10))
|
||||
or can_shoot_arrows(state, player, 35) or state.has("Silver Bow", player)
|
||||
or has_melee_weapon(state, player))
|
||||
|
||||
|
||||
def TrinexxDefeatRule(state, player: int) -> bool:
|
||||
|
||||
@@ -484,8 +484,7 @@ def generate_itempool(world):
|
||||
if multiworld.randomize_cost_types[player]:
|
||||
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
|
||||
for item in items:
|
||||
if (item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart")
|
||||
or "Arrow Upgrade" in item.name):
|
||||
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
|
||||
item.classification = ItemClassification.progression
|
||||
else:
|
||||
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
|
||||
@@ -713,7 +712,7 @@ def get_pool_core(world, player: int):
|
||||
pool.remove("Rupees (20)")
|
||||
|
||||
if retro_bow:
|
||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (50)'}
|
||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
|
||||
pool = ['Rupees (5)' if item in replace else item for item in pool]
|
||||
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
||||
pool.extend(diff.universal_keys)
|
||||
|
||||
@@ -7,7 +7,7 @@ from worlds.AutoWorld import World
|
||||
def GetBeemizerItem(world, player: int, item):
|
||||
item_name = item if isinstance(item, str) else item.name
|
||||
|
||||
if item_name not in trap_replaceable:
|
||||
if item_name not in trap_replaceable or player in world.groups:
|
||||
return item
|
||||
|
||||
# first roll - replaceable item should be replaced, within beemizer_total_chance
|
||||
@@ -110,9 +110,9 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
|
||||
'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
|
||||
'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'),
|
||||
'Arrow Upgrade (+10)': ItemData(IC.useful, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (+5)': ItemData(IC.useful, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (70)': ItemData(IC.useful, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (+10)': ItemData(IC.progression_skip_balancing, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (+5)': ItemData(IC.progression_skip_balancing, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (70)': ItemData(IC.progression_skip_balancing, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'),
|
||||
'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
|
||||
'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
|
||||
|
||||
@@ -170,7 +170,8 @@ def push_shop_inventories(multiworld):
|
||||
# Retro Bow arrows will already have been pushed
|
||||
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
|
||||
!= ("Single Arrow", location.player)):
|
||||
location.shop.push_inventory(location.shop_slot, item_name, location.shop_price,
|
||||
location.shop.push_inventory(location.shop_slot, item_name,
|
||||
round(location.shop_price * get_price_modifier(location.item)),
|
||||
1, location.item.player if location.item.player != location.player else 0,
|
||||
location.shop_price_type)
|
||||
location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price,
|
||||
|
||||
@@ -15,18 +15,18 @@ def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bo
|
||||
|
||||
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
|
||||
shop in state.multiworld.shops)
|
||||
shop in state.multiworld.shops)
|
||||
|
||||
|
||||
def can_buy(state: CollectionState, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
|
||||
shop in state.multiworld.shops)
|
||||
shop in state.multiworld.shops)
|
||||
|
||||
|
||||
def can_shoot_arrows(state: CollectionState, player: int) -> bool:
|
||||
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
|
||||
if state.multiworld.retro_bow[player]:
|
||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
|
||||
return state.has('Bow', player) or state.has('Silver Bow', player)
|
||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
|
||||
|
||||
|
||||
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
|
||||
@@ -61,13 +61,13 @@ def heart_count(state: CollectionState, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = state.multiworld.worlds[player].difficulty_requirements
|
||||
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
+ 3 # starting hearts
|
||||
+ 3 # starting hearts
|
||||
|
||||
|
||||
def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
|
||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
||||
basemagic = 8
|
||||
if state.has('Magic Upgrade (1/4)', player):
|
||||
basemagic = 32
|
||||
@@ -84,11 +84,18 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
|
||||
|
||||
|
||||
def can_hold_arrows(state: CollectionState, player: int, quantity: int):
|
||||
arrows = 30 + ((state.count("Arrow Upgrade (+5)", player) * 5) + (state.count("Arrow Upgrade (+10)", player) * 10)
|
||||
+ (state.count("Bomb Upgrade (50)", player) * 50))
|
||||
# Arrow Upgrade (+5) beyond the 6th gives +10
|
||||
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
|
||||
return min(70, arrows) >= quantity
|
||||
if state.multiworld.worlds[player].options.shuffle_capacity_upgrades:
|
||||
if quantity == 0:
|
||||
return True
|
||||
if state.has("Arrow Upgrade (70)", player):
|
||||
arrows = 70
|
||||
else:
|
||||
arrows = (30 + (state.count("Arrow Upgrade (+5)", player) * 5)
|
||||
+ (state.count("Arrow Upgrade (+10)", player) * 10))
|
||||
# Arrow Upgrade (+5) beyond the 6th gives +10
|
||||
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
|
||||
return min(70, arrows) >= quantity
|
||||
return quantity <= 30 or state.has("Capacity Upgrade Shop", player)
|
||||
|
||||
|
||||
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
|
||||
@@ -146,19 +153,19 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
|
||||
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
|
||||
(state.multiworld.swordless[player] and
|
||||
state.has("Hammer", player)))
|
||||
state.has("Hammer", player)))
|
||||
|
||||
|
||||
def has_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fighter Sword', player) \
|
||||
or state.has('Master Sword', player) \
|
||||
or state.has('Tempered Sword', player) \
|
||||
or state.has('Golden Sword', player)
|
||||
or state.has('Master Sword', player) \
|
||||
or state.has('Tempered Sword', player) \
|
||||
or state.has('Golden Sword', player)
|
||||
|
||||
|
||||
def has_beam_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword',
|
||||
player)
|
||||
player)
|
||||
|
||||
|
||||
def has_melee_weapon(state: CollectionState, player: int) -> bool:
|
||||
@@ -171,9 +178,9 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
|
||||
|
||||
def can_melt_things(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fire Rod', player) or \
|
||||
(state.has('Bombos', player) and
|
||||
(state.multiworld.swordless[player] or
|
||||
has_sword(state, player)))
|
||||
(state.has('Bombos', player) and
|
||||
(state.multiworld.swordless[player] or
|
||||
has_sword(state, player)))
|
||||
|
||||
|
||||
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:
|
||||
|
||||
@@ -77,5 +77,5 @@ class TestMiseryMire(TestDungeon):
|
||||
["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Arrow Upgrade (+5)', 'Pegasus Boots']],
|
||||
])
|
||||
@@ -93,7 +93,7 @@ class AquariaWorld(World):
|
||||
options: AquariaOptions
|
||||
"Every options of the world"
|
||||
|
||||
regions: AquariaRegions
|
||||
regions: AquariaRegions | None
|
||||
"Used to manage Regions"
|
||||
|
||||
exclude: List[str]
|
||||
@@ -101,10 +101,17 @@ class AquariaWorld(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
"""Initialisation of the Aquaria World"""
|
||||
super(AquariaWorld, self).__init__(multiworld, player)
|
||||
self.regions = AquariaRegions(multiworld, player)
|
||||
self.regions = None
|
||||
self.ingredients_substitution = []
|
||||
self.exclude = []
|
||||
|
||||
def generate_early(self) -> None:
|
||||
"""
|
||||
Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
|
||||
results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
|
||||
"""
|
||||
self.regions = AquariaRegions(self.multiworld, self.player)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
"""
|
||||
Create every Region in `regions`
|
||||
|
||||
@@ -103,6 +103,9 @@ class BlasphemousWorld(World):
|
||||
if not self.options.wall_climb_shuffle:
|
||||
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if self.options.thorn_shuffle == "local_only":
|
||||
self.options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
if not self.options.boots_of_pleading:
|
||||
self.disabled_locations.append("RE401")
|
||||
|
||||
@@ -200,9 +203,6 @@ class BlasphemousWorld(World):
|
||||
|
||||
if not self.options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if self.options.thorn_shuffle == "local_only":
|
||||
self.options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
@@ -32,7 +31,7 @@ class DKC3SNIClient(SNIClient):
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
from SNIClient import snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3":
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from BaseClasses import Item
|
||||
from .Names import ItemName
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
|
||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
||||
from Options import Choice, Range, Toggle, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance
|
||||
from .Items import DKC3Item
|
||||
from BaseClasses import Region, Entrance
|
||||
from worlds.AutoWorld import World
|
||||
from .Locations import DKC3Location
|
||||
from .Names import LocationName, ItemName
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
def create_regions(world: World, active_locations):
|
||||
|
||||
@@ -2,7 +2,6 @@ import Utils
|
||||
from Utils import read_snes_rom
|
||||
from worlds.AutoWorld import World
|
||||
from worlds.Files import APDeltaPatch
|
||||
from .Locations import lookup_id_to_name, all_locations
|
||||
from .Levels import level_list, level_dict
|
||||
|
||||
USHASH = '120abf304f0c40fe059f6a192ed4f947'
|
||||
@@ -436,7 +435,7 @@ level_music_ids = [
|
||||
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
def __init__(self, file, name=None, hash=None):
|
||||
self.name = name
|
||||
self.hash = hash
|
||||
self.orig_buffer = None
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import math
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from worlds.generic.Rules import add_rule
|
||||
from .Names import LocationName, ItemName
|
||||
from worlds.AutoWorld import LogicMixin, World
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
|
||||
|
||||
def set_rules(world: World):
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import dataclasses
|
||||
import os
|
||||
import typing
|
||||
import math
|
||||
import os
|
||||
import threading
|
||||
import typing
|
||||
|
||||
import settings
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from Options import PerGameCommonOptions
|
||||
import Patch
|
||||
import settings
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .Client import DKC3SNIClient
|
||||
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
|
||||
from .Levels import level_list
|
||||
|
||||
@@ -3,13 +3,23 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
|
||||
from schema import Schema, Optional, And, Or
|
||||
from schema import Schema, Optional, And, Or, SchemaError
|
||||
|
||||
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool, PerGameCommonOptions, OptionGroup
|
||||
|
||||
# schema helpers
|
||||
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
|
||||
class FloatRange:
|
||||
def __init__(self, low, high):
|
||||
self._low = low
|
||||
self._high = high
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, (float, int)):
|
||||
raise SchemaError(f"should be instance of float or int, but was {value!r}")
|
||||
if not self._low <= value <= self._high:
|
||||
raise SchemaError(f"{value} is not between {self._low} and {self._high}")
|
||||
|
||||
LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||
|
||||
|
||||
|
||||
@@ -717,8 +717,10 @@ TRAP_TABLE = {
|
||||
game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25)
|
||||
end,
|
||||
["Evolution Trap"] = function ()
|
||||
game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor))
|
||||
game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor})
|
||||
local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") +
|
||||
(TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis")))
|
||||
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
|
||||
game.print({"", "New evolution factor:", new_factor})
|
||||
end,
|
||||
["Teleport Trap"] = function ()
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
|
||||
39
worlds/factorio/test_file_validation.py
Normal file
39
worlds/factorio/test_file_validation.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Tests for error messages from YAML validation."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import WebHostLib.check
|
||||
|
||||
FACTORIO_YAML="""
|
||||
game: Factorio
|
||||
Factorio:
|
||||
world_gen:
|
||||
autoplace_controls:
|
||||
coal:
|
||||
richness: 1
|
||||
frequency: {}
|
||||
size: 1
|
||||
"""
|
||||
|
||||
def yamlWithFrequency(f):
|
||||
return FACTORIO_YAML.format(f)
|
||||
|
||||
|
||||
class TestFileValidation(unittest.TestCase):
|
||||
def test_out_of_range(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1000)})
|
||||
self.assertIn("between 0 and 6", results["bob.yaml"])
|
||||
|
||||
def test_bad_non_numeric(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency("not numeric")})
|
||||
self.assertIn("float", results["bob.yaml"])
|
||||
self.assertIn("int", results["bob.yaml"])
|
||||
|
||||
def test_good_float(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1.0)})
|
||||
self.assertIs(results["bob.yaml"], True)
|
||||
|
||||
def test_good_int(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1)})
|
||||
self.assertIs(results["bob.yaml"], True)
|
||||
@@ -44,8 +44,13 @@ class FaxanaduWorld(World):
|
||||
location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None}
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.filler_ratios: Dict[str, int] = {}
|
||||
|
||||
self.filler_ratios: Dict[str, int] = {
|
||||
item.name: item.count
|
||||
for item in Items.items
|
||||
if item.classification in [ItemClassification.filler, ItemClassification.trap]
|
||||
}
|
||||
# Remove poison by default to respect itemlinking
|
||||
self.filler_ratios["Poison"] = 0
|
||||
super().__init__(world, player)
|
||||
|
||||
def create_regions(self):
|
||||
@@ -160,19 +165,13 @@ class FaxanaduWorld(World):
|
||||
for i in range(item.progression_count):
|
||||
itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player))
|
||||
|
||||
# Set up filler ratios
|
||||
self.filler_ratios = {
|
||||
item.name: item.count
|
||||
for item in Items.items
|
||||
if item.classification in [ItemClassification.filler, ItemClassification.trap]
|
||||
}
|
||||
|
||||
# Adjust filler ratios
|
||||
# If red potions are locked in shops, remove the count from the ratio.
|
||||
self.filler_ratios["Red Potion"] -= red_potion_in_shop_count
|
||||
|
||||
# Remove poisons if not desired
|
||||
if not self.options.include_poisons:
|
||||
self.filler_ratios["Poison"] = 0
|
||||
# Add poisons if desired
|
||||
if self.options.include_poisons:
|
||||
self.filler_ratios["Poison"] = self.item_name_to_item["Poison"].count
|
||||
|
||||
# Randomly add fillers to the pool with ratios based on og game occurrence counts.
|
||||
filler_count = len(Locations.locations) - len(itempool) - prefilled_count
|
||||
|
||||
@@ -181,6 +181,7 @@ class HKWorld(World):
|
||||
charm_costs: typing.List[int]
|
||||
cached_filler_items = {}
|
||||
grub_count: int
|
||||
grub_player_count: typing.Dict[int, int]
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super(HKWorld, self).__init__(multiworld, player)
|
||||
@@ -190,7 +191,6 @@ class HKWorld(World):
|
||||
self.ranges = {}
|
||||
self.created_shop_items = 0
|
||||
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
|
||||
self.grub_count = 0
|
||||
|
||||
def generate_early(self):
|
||||
options = self.options
|
||||
@@ -204,7 +204,14 @@ class HKWorld(World):
|
||||
mini.value = min(mini.value, maxi.value)
|
||||
self.ranges[term] = mini.value, maxi.value
|
||||
self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key],
|
||||
True, None, "Event", self.player))
|
||||
True, None, "Event", self.player))
|
||||
|
||||
# defaulting so completion condition isn't incorrect before pre_fill
|
||||
self.grub_count = (
|
||||
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
|
||||
else options.GrubHuntGoal
|
||||
)
|
||||
self.grub_player_count = {self.player: self.grub_count}
|
||||
|
||||
def white_palace_exclusions(self):
|
||||
exclusions = set()
|
||||
@@ -469,25 +476,20 @@ class HKWorld(World):
|
||||
elif goal == Goal.option_godhome_flower:
|
||||
multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
|
||||
elif goal == Goal.option_grub_hunt:
|
||||
pass # will set in stage_pre_fill()
|
||||
multiworld.completion_condition[player] = lambda state: self.can_grub_goal(state)
|
||||
else:
|
||||
# Any goal
|
||||
multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \
|
||||
_hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player)
|
||||
_hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) and \
|
||||
self.can_grub_goal(state)
|
||||
|
||||
set_rules(self)
|
||||
|
||||
def can_grub_goal(self, state: CollectionState) -> bool:
|
||||
return all(state.has("Grub", owner, count) for owner, count in self.grub_player_count.items())
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, multiworld: "MultiWorld"):
|
||||
def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]):
|
||||
world = multiworld.worlds[player]
|
||||
|
||||
if world.options.Goal == "grub_hunt":
|
||||
multiworld.completion_condition[player] = grub_rule
|
||||
else:
|
||||
old_rule = multiworld.completion_condition[player]
|
||||
multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state)
|
||||
|
||||
worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]]
|
||||
if worlds:
|
||||
grubs = [item for item in multiworld.get_items() if item.name == "Grub"]
|
||||
@@ -525,13 +527,13 @@ class HKWorld(World):
|
||||
|
||||
for player, grub_player_count in per_player_grubs_per_player.items():
|
||||
if player in all_grub_players:
|
||||
set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items()))
|
||||
multiworld.worlds[player].grub_player_count = grub_player_count
|
||||
|
||||
for world in worlds:
|
||||
if world.player not in all_grub_players:
|
||||
world.grub_count = world.options.GrubHuntGoal.value
|
||||
player = world.player
|
||||
set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c))
|
||||
world.grub_player_count = {player: world.grub_count}
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
|
||||
@@ -68,10 +68,12 @@ DEFAULT_ITEM_POOL = {
|
||||
|
||||
|
||||
class ItemPool:
|
||||
def __init__(self, logic, settings, rnd):
|
||||
def __init__(self, logic, settings, rnd, stabilize_item_pool: bool):
|
||||
self.__pool = {}
|
||||
self.__setup(logic, settings)
|
||||
self.__randomizeRupees(settings, rnd)
|
||||
|
||||
if not stabilize_item_pool:
|
||||
self.__randomizeRupees(settings, rnd)
|
||||
|
||||
def add(self, item, count=1):
|
||||
self.__pool[item] = self.__pool.get(item, 0) + count
|
||||
|
||||
@@ -527,6 +527,13 @@ class InGameHints(DefaultOnToggle):
|
||||
display_name = "In-game Hints"
|
||||
|
||||
|
||||
class StabilizeItemPool(DefaultOffToggle):
|
||||
"""
|
||||
By default, rupees in the item pool may be randomly swapped with bombs, arrows, powders, or capacity upgrades. This option disables that swapping, which is useful for plando.
|
||||
"""
|
||||
display_name = "Stabilize Item Pool"
|
||||
|
||||
|
||||
class ForeignItemIcons(Choice):
|
||||
"""
|
||||
Choose how to display foreign items.
|
||||
@@ -562,6 +569,7 @@ ladx_option_groups = [
|
||||
TrendyGame,
|
||||
InGameHints,
|
||||
NagMessages,
|
||||
StabilizeItemPool,
|
||||
Quickswap,
|
||||
HardMode,
|
||||
BootsControls
|
||||
@@ -631,6 +639,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
|
||||
no_flash: NoFlash
|
||||
in_game_hints: InGameHints
|
||||
overworld: Overworld
|
||||
stabilize_item_pool: StabilizeItemPool
|
||||
|
||||
warp_improvements: Removed
|
||||
additional_warp_points: Removed
|
||||
|
||||
@@ -138,7 +138,30 @@ class LinksAwakeningWorld(World):
|
||||
world_setup = LADXRWorldSetup()
|
||||
world_setup.randomize(self.ladxr_settings, self.random)
|
||||
self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup)
|
||||
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict()
|
||||
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random, bool(self.options.stabilize_item_pool)).toDict()
|
||||
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.dungeon_item_types = {
|
||||
}
|
||||
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||
option_name = "shuffle_" + dungeon_item_type
|
||||
option: DungeonItemShuffle = getattr(self.options, option_name)
|
||||
|
||||
self.dungeon_item_types[option.ladxr_item] = option.value
|
||||
|
||||
# The color dungeon does not contain an instrument
|
||||
num_items = 8 if dungeon_item_type == "instruments" else 9
|
||||
|
||||
# For any and different world, set item rule instead
|
||||
if option.value == DungeonItemShuffle.option_own_world:
|
||||
self.options.local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
elif option.value == DungeonItemShuffle.option_different_world:
|
||||
self.options.non_local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Initialize
|
||||
@@ -185,32 +208,9 @@ class LinksAwakeningWorld(World):
|
||||
def create_items(self) -> None:
|
||||
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
dungeon_item_types = {
|
||||
|
||||
}
|
||||
|
||||
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
|
||||
self.prefill_own_dungeons = []
|
||||
self.pre_fill_items = []
|
||||
# For any and different world, set item rule instead
|
||||
|
||||
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||
option_name = "shuffle_" + dungeon_item_type
|
||||
option: DungeonItemShuffle = getattr(self.options, option_name)
|
||||
|
||||
dungeon_item_types[option.ladxr_item] = option.value
|
||||
|
||||
# The color dungeon does not contain an instrument
|
||||
num_items = 8 if dungeon_item_type == "instruments" else 9
|
||||
|
||||
if option.value == DungeonItemShuffle.option_own_world:
|
||||
self.options.local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
elif option.value == DungeonItemShuffle.option_different_world:
|
||||
self.options.non_local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
# option_original_dungeon = 0
|
||||
# option_own_dungeons = 1
|
||||
# option_own_world = 2
|
||||
@@ -226,7 +226,7 @@ class LinksAwakeningWorld(World):
|
||||
for _ in range(count):
|
||||
if item_name in exclude:
|
||||
exclude.remove(item_name) # this is destructive. create unique list above
|
||||
self.multiworld.itempool.append(self.create_item("Nothing"))
|
||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
else:
|
||||
item = self.create_item(item_name)
|
||||
|
||||
@@ -238,7 +238,7 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
if isinstance(item.item_data, DungeonItemData):
|
||||
item_type = item.item_data.ladxr_id[:-1]
|
||||
shuffle_type = dungeon_item_types[item_type]
|
||||
shuffle_type = self.dungeon_item_types[item_type]
|
||||
|
||||
if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
|
||||
# Find instrument, lock
|
||||
@@ -500,8 +500,14 @@ class LinksAwakeningWorld(World):
|
||||
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
|
||||
return change
|
||||
|
||||
# Same fill choices and weights used in LADXR.itempool.__randomizeRupees
|
||||
filler_choices = ("Bomb", "Single Arrow", "10 Arrows", "Magic Powder", "Medicine")
|
||||
filler_weights = ( 10, 5, 10, 10, 1)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Nothing"
|
||||
if self.options.stabilize_item_pool:
|
||||
return "Nothing"
|
||||
return self.random.choices(self.filler_choices, self.filler_weights)[0]
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
|
||||
@@ -128,6 +128,9 @@ class LingoWorld(World):
|
||||
pool.append(self.create_item("Puzzle Skip"))
|
||||
|
||||
if traps:
|
||||
if self.options.speed_boost_mode:
|
||||
self.options.trap_weights.value["Slowness Trap"] = 0
|
||||
|
||||
total_weight = sum(self.options.trap_weights.values())
|
||||
|
||||
if total_weight == 0:
|
||||
@@ -171,7 +174,7 @@ class LingoWorld(World):
|
||||
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
||||
"enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
|
||||
"early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps",
|
||||
"group_doors"
|
||||
"group_doors", "speed_boost_mode"
|
||||
]
|
||||
|
||||
slot_data = {
|
||||
@@ -188,5 +191,8 @@ class LingoWorld(World):
|
||||
return slot_data
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
return self.random.choice(filler_list)
|
||||
if self.options.speed_boost_mode:
|
||||
return "Speed Boost"
|
||||
else:
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
return self.random.choice(filler_list)
|
||||
|
||||
Binary file not shown.
@@ -17,6 +17,7 @@ special_items:
|
||||
Iceland Trap: 444411
|
||||
Atbash Trap: 444412
|
||||
Puzzle Skip: 444413
|
||||
Speed Boost: 444680
|
||||
panels:
|
||||
Starting Room:
|
||||
HI: 444400
|
||||
|
||||
@@ -85,6 +85,7 @@ def load_item_data():
|
||||
"The Feeling of Being Lost": ItemClassification.filler,
|
||||
"Wanderlust": ItemClassification.filler,
|
||||
"Empty White Hallways": ItemClassification.filler,
|
||||
"Speed Boost": ItemClassification.filler,
|
||||
**{trap_name: ItemClassification.trap for trap_name in TRAP_ITEMS},
|
||||
"Puzzle Skip": ItemClassification.useful,
|
||||
}
|
||||
|
||||
@@ -232,6 +232,14 @@ class TrapWeights(OptionDict):
|
||||
default = {trap_name: 1 for trap_name in TRAP_ITEMS}
|
||||
|
||||
|
||||
class SpeedBoostMode(Toggle):
|
||||
"""
|
||||
If on, the player's default speed is halved, as if affected by a Slowness Trap. Speed Boosts are added to
|
||||
the item pool, which temporarily return the player to normal speed. Slowness Traps are removed from the pool.
|
||||
"""
|
||||
display_name = "Speed Boost Mode"
|
||||
|
||||
|
||||
class PuzzleSkipPercentage(Range):
|
||||
"""Replaces junk items with puzzle skips, at the specified rate."""
|
||||
display_name = "Puzzle Skip Percentage"
|
||||
@@ -260,6 +268,7 @@ lingo_option_groups = [
|
||||
Level2Requirement,
|
||||
TrapPercentage,
|
||||
TrapWeights,
|
||||
SpeedBoostMode,
|
||||
PuzzleSkipPercentage,
|
||||
])
|
||||
]
|
||||
@@ -287,6 +296,7 @@ class LingoOptions(PerGameCommonOptions):
|
||||
shuffle_postgame: ShufflePostgame
|
||||
trap_percentage: TrapPercentage
|
||||
trap_weights: TrapWeights
|
||||
speed_boost_mode: SpeedBoostMode
|
||||
puzzle_skip_percentage: PuzzleSkipPercentage
|
||||
death_link: DeathLink
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
@@ -59,4 +59,11 @@ class TestShuffleSunwarpsAccess(LingoTestBase):
|
||||
"victory_condition": "pilgrimage",
|
||||
"shuffle_sunwarps": "true",
|
||||
"sunwarp_access": "individual"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestSpeedBoostMode(LingoTestBase):
|
||||
options = {
|
||||
"location_checks": "insanity",
|
||||
"speed_boost_mode": "true",
|
||||
}
|
||||
|
||||
@@ -216,3 +216,6 @@ config.each do |room_name, room_data|
|
||||
end
|
||||
|
||||
File.write(outputpath, old_generated.to_yaml)
|
||||
|
||||
puts "Next item ID: #{next_item_id}"
|
||||
puts "Next location ID: #{next_location_id}"
|
||||
|
||||
@@ -2200,7 +2200,7 @@ def patch_rom(world, rom):
|
||||
elif world.shuffle_bosses != 'off':
|
||||
vanilla_reward = world.get_location(boss_name).vanilla_item
|
||||
vanilla_reward_location = world.multiworld.find_item(vanilla_reward, world.player) # hinted_dungeon_reward_locations[vanilla_reward.name]
|
||||
area = HintArea.at(vanilla_reward_location).text(world.clearer_hints, preposition=True)
|
||||
area = HintArea.at(vanilla_reward_location).text(world.hint_rng, world.clearer_hints, preposition=True)
|
||||
compass_message = "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for %s\x05\x40!\x01The %s can be found\x01%s!\x09" % (dungeon_name, vanilla_reward, area)
|
||||
else:
|
||||
boss_location = next(filter(lambda loc: loc.type == 'Boss', world.get_entrance(f'{dungeon} Boss Door -> {boss_name} Boss Room').connected_region.locations))
|
||||
|
||||
@@ -582,8 +582,7 @@ class OOTWorld(World):
|
||||
new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region)
|
||||
new_exit.vanilla_connected_region = exit
|
||||
new_exit.rule_string = rule
|
||||
if self.options.logic_rules != 'no_logic':
|
||||
self.parser.parse_spot_rule(new_exit)
|
||||
self.parser.parse_spot_rule(new_exit)
|
||||
if new_exit.never:
|
||||
logger.debug('Dropping unreachable exit: %s', new_exit.name)
|
||||
else:
|
||||
|
||||
@@ -11,7 +11,7 @@ from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor,
|
||||
from ..strings.crop_names import Fruit, Vegetable
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
|
||||
from ..strings.fish_names import Fish, WaterItem, ModTrash
|
||||
from ..strings.fish_names import Fish, WaterItem, ModTrash, Trash
|
||||
from ..strings.flower_names import Flower
|
||||
from ..strings.food_names import Meal
|
||||
from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
|
||||
@@ -378,4 +378,12 @@ recycling_bin = skill_recipe(ModMachine.recycling_bin, ModSkill.binning, 7, {Met
|
||||
advanced_recycling_machine = skill_recipe(ModMachine.advanced_recycling_machine, ModSkill.binning, 9,
|
||||
{MetalBar.iridium: 5, ArtisanGood.battery_pack: 2, MetalBar.quartz: 10}, ModNames.binning_skill)
|
||||
|
||||
coppper_slot_machine = skill_recipe(ModMachine.copper_slot_machine, ModSkill.luck, 2, {MetalBar.copper: 15, Material.stone: 1, Material.wood: 1,
|
||||
Material.fiber: 1, Material.sap: 1, Loot.slime: 1,
|
||||
Forageable.salmonberry: 1, Material.clay: 1, Trash.joja_cola: 1}, ModNames.luck_skill)
|
||||
|
||||
gold_slot_machine = skill_recipe(ModMachine.gold_slot_machine, ModSkill.luck, 4, {MetalBar.gold: 15, ModMachine.copper_slot_machine: 1}, ModNames.luck_skill)
|
||||
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 4, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
|
||||
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 4, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
|
||||
|
||||
all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes}
|
||||
|
||||
@@ -2935,6 +2935,10 @@ id,region,name,tags,mod_name
|
||||
7433,Farm,Craft Composter,CRAFTSANITY,Binning Skill
|
||||
7434,Farm,Craft Recycling Bin,CRAFTSANITY,Binning Skill
|
||||
7435,Farm,Craft Advanced Recycling Machine,CRAFTSANITY,Binning Skill
|
||||
7440,Farm,Craft Copper Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7441,Farm,Craft Gold Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7442,Farm,Craft Iridium Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY",Luck Skill
|
||||
7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
|
||||
7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic
|
||||
7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded
|
||||
@@ -3241,7 +3245,7 @@ id,region,name,tags,mod_name
|
||||
8199,Shipping,Shipsanity: Hardwood Display,SHIPSANITY,Archaeology
|
||||
8200,Shipping,Shipsanity: Wooden Display,SHIPSANITY,Archaeology
|
||||
8201,Shipping,Shipsanity: Dwarf Gadget: Infinite Volcano Simulation,"SHIPSANITY,GINGER_ISLAND",Archaeology
|
||||
8202,Shipping,Shipsanity: Water Shifter,SHIPSANITY,Archaeology
|
||||
8202,Shipping,Shipsanity: Water Shifter,"SHIPSANITY,DEPRECATED",Archaeology
|
||||
8203,Shipping,Shipsanity: Brown Amanita,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Distant Lands - Witch Swamp Overhaul
|
||||
8204,Shipping,Shipsanity: Swamp Herb,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Distant Lands - Witch Swamp Overhaul
|
||||
8205,Shipping,Shipsanity: Void Mint Seeds,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
|
||||
|
||||
|
@@ -110,6 +110,8 @@ class LocationTags(enum.Enum):
|
||||
MAGIC_LEVEL = enum.auto()
|
||||
ARCHAEOLOGY_LEVEL = enum.auto()
|
||||
|
||||
DEPRECATED = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocationData:
|
||||
@@ -519,6 +521,10 @@ def create_locations(location_collector: StardewLocationCollector,
|
||||
location_collector(location_data.name, location_data.code, location_data.region)
|
||||
|
||||
|
||||
def filter_deprecated_locations(locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
||||
return [location for location in locations if LocationTags.DEPRECATED not in location.tags]
|
||||
|
||||
|
||||
def filter_farm_type(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
||||
# On Meadowlands, "Feeding Animals" replaces "Raising Animals"
|
||||
if options.farm_type == FarmType.option_meadowlands:
|
||||
@@ -549,7 +555,8 @@ def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[L
|
||||
|
||||
|
||||
def filter_disabled_locations(options: StardewValleyOptions, content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
||||
locations_farm_filter = filter_farm_type(options, locations)
|
||||
locations_deprecated_filter = filter_deprecated_locations(locations)
|
||||
locations_farm_filter = filter_farm_type(options, locations_deprecated_filter)
|
||||
locations_island_filter = filter_ginger_island(options, locations_farm_filter)
|
||||
locations_qi_filter = filter_qi_order_locations(options, locations_island_filter)
|
||||
locations_masteries_filter = filter_masteries_locations(content, locations_qi_filter)
|
||||
|
||||
@@ -757,6 +757,14 @@ class Gifting(Toggle):
|
||||
default = 1
|
||||
|
||||
|
||||
all_mods = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands,
|
||||
ModNames.alecto, ModNames.lacey, ModNames.boarding_house}
|
||||
|
||||
# These mods have been disabled because either they are not updated for the current supported version of Stardew Valley,
|
||||
# or we didn't find the time to validate that they work or fix compatibility issues if they do.
|
||||
# Once a mod is validated to be functional, it can simply be removed from this list
|
||||
@@ -766,8 +774,7 @@ disabled_mods = {ModNames.deepwoods, ModNames.magic,
|
||||
ModNames.wellwick, ModNames.shiko, ModNames.delores, ModNames.riley,
|
||||
ModNames.boarding_house}
|
||||
|
||||
if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys():
|
||||
disabled_mods = {}
|
||||
enabled_mods = all_mods.difference(disabled_mods)
|
||||
|
||||
|
||||
class Mods(OptionSet):
|
||||
@@ -775,13 +782,11 @@ class Mods(OptionSet):
|
||||
visibility = Visibility.all & ~Visibility.simple_ui
|
||||
internal_name = "mods"
|
||||
display_name = "Mods"
|
||||
valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands,
|
||||
ModNames.alecto, ModNames.lacey, ModNames.boarding_house}.difference(disabled_mods)
|
||||
valid_keys = enabled_mods
|
||||
# In tests, we keep even the disabled mods active, because we expect some of them to eventually get updated for SV 1.6
|
||||
# In that case, we want to maintain content and logic for them, and therefore keep testing them
|
||||
if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys():
|
||||
valid_keys = all_mods
|
||||
|
||||
|
||||
class BundlePlando(OptionSet):
|
||||
|
||||
@@ -201,6 +201,10 @@ class ModMachine:
|
||||
composter = "Composter"
|
||||
recycling_bin = "Recycling Bin"
|
||||
advanced_recycling_machine = "Advanced Recycling Machine"
|
||||
copper_slot_machine = "Copper Slot Machine"
|
||||
gold_slot_machine = "Gold Slot Machine"
|
||||
iridium_slot_machine = "Iridium Slot Machine"
|
||||
radioactive_slot_machine = "Radioactive Slot Machine"
|
||||
|
||||
|
||||
class ModFloor:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from . import SVTestBase, allsanity_no_mods_6_x_x, \
|
||||
allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x
|
||||
allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x, \
|
||||
allsanity_mods_6_x_x_exclude_disabled
|
||||
from .. import location_table
|
||||
from ..items import Group, item_table
|
||||
|
||||
@@ -70,7 +71,7 @@ class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase):
|
||||
options = allsanity_no_mods_6_x_x()
|
||||
|
||||
def test_allsanity_without_mods_has_at_least_locations(self):
|
||||
expected_locations = 2238
|
||||
expected_locations = 2256
|
||||
real_locations = self.get_real_locations()
|
||||
number_locations = len(real_locations)
|
||||
print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}")
|
||||
@@ -83,10 +84,10 @@ class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase):
|
||||
|
||||
|
||||
class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase):
|
||||
options = allsanity_mods_6_x_x()
|
||||
options = allsanity_mods_6_x_x_exclude_disabled()
|
||||
|
||||
def test_allsanity_with_mods_has_at_least_locations(self):
|
||||
expected_locations = 3096
|
||||
expected_locations = 2908
|
||||
real_locations = self.get_real_locations()
|
||||
number_locations = len(real_locations)
|
||||
print(f"Stardew Valley - Allsanity Locations with all mods: {number_locations}")
|
||||
|
||||
@@ -13,6 +13,7 @@ from .assertion import RuleAssertMixin
|
||||
from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default
|
||||
from .. import StardewValleyWorld, options, StardewItem
|
||||
from ..options import StardewValleyOption
|
||||
from ..options.options import enabled_mods
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -98,6 +99,12 @@ def allsanity_mods_6_x_x():
|
||||
return allsanity
|
||||
|
||||
|
||||
def allsanity_mods_6_x_x_exclude_disabled():
|
||||
allsanity = allsanity_no_mods_6_x_x()
|
||||
allsanity.update({options.Mods.internal_name: frozenset(enabled_mods)})
|
||||
return allsanity
|
||||
|
||||
|
||||
def get_minsanity_options():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
|
||||
|
||||
@@ -40,7 +40,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
### Where do I get a config file?
|
||||
|
||||
The Player Options page on the website allows you to configure your personal options and export a config file from
|
||||
them. Player options page: [The Legend of Zelda Player Sptions Page](/games/The%20Legend%20of%20Zelda/player-options)
|
||||
them. Player options page: [The Legend of Zelda Player Options Page](/games/The%20Legend%20of%20Zelda/player-options)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
|
||||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set
|
||||
from logging import warning
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
||||
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
|
||||
combat_items)
|
||||
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
|
||||
from .locations import location_table, location_name_groups, standard_location_name_to_id, hexagon_locations, sphere_one
|
||||
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
|
||||
from .er_rules import set_er_location_rules
|
||||
from .regions import tunic_regions
|
||||
from .er_scripts import create_er_regions
|
||||
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
|
||||
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
||||
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
|
||||
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage)
|
||||
from .combat_logic import area_data, CombatState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Options import PlandoConnection
|
||||
from Options import PlandoConnection, OptionError
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from settings import Group, Bool
|
||||
|
||||
@@ -22,7 +23,11 @@ class TunicSettings(Group):
|
||||
class DisableLocalSpoiler(Bool):
|
||||
"""Disallows the TUNIC client from creating a local spoiler log."""
|
||||
|
||||
class LimitGrassRando(Bool):
|
||||
"""Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95."""
|
||||
|
||||
disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
|
||||
limit_grass_rando: Union[LimitGrassRando, bool] = True
|
||||
|
||||
|
||||
class TunicWeb(WebWorld):
|
||||
@@ -73,10 +78,13 @@ class TunicWorld(World):
|
||||
settings: ClassVar[TunicSettings]
|
||||
item_name_groups = item_name_groups
|
||||
location_name_groups = location_name_groups
|
||||
location_name_groups.update(grass_location_name_groups)
|
||||
|
||||
item_name_to_id = item_name_to_id
|
||||
location_name_to_id = location_name_to_id
|
||||
location_name_to_id = standard_location_name_to_id.copy()
|
||||
location_name_to_id.update(grass_location_name_to_id)
|
||||
|
||||
player_location_table: Dict[str, int]
|
||||
ability_unlocks: Dict[str, int]
|
||||
slot_data_items: List[TunicItem]
|
||||
tunic_portal_pairs: Dict[str, str]
|
||||
@@ -85,6 +93,11 @@ 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
|
||||
|
||||
# for the local_fill option
|
||||
fill_items: List[TunicItem]
|
||||
fill_locations: List[TunicLocation]
|
||||
amount_to_local_fill: int
|
||||
|
||||
# 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]]]] = {}
|
||||
@@ -132,6 +145,7 @@ class TunicWorld(World):
|
||||
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
|
||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_false
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = self.passthrough["combat_logic"]
|
||||
@@ -140,6 +154,22 @@ class TunicWorld(World):
|
||||
else:
|
||||
self.using_ut = False
|
||||
|
||||
self.player_location_table = standard_location_name_to_id.copy()
|
||||
|
||||
if self.options.local_fill == -1:
|
||||
if self.options.grass_randomizer:
|
||||
self.options.local_fill.value = 95
|
||||
else:
|
||||
self.options.local_fill.value = 0
|
||||
|
||||
if self.options.grass_randomizer:
|
||||
if self.settings.limit_grass_rando and self.options.local_fill < 95 and self.multiworld.players > 1:
|
||||
raise OptionError(f"TUNIC: Player {self.player_name} has their Local Fill option set too low. "
|
||||
f"They must either bring it above 95% or the host needs to disable limit_grass_rando "
|
||||
f"in their host.yaml settings")
|
||||
|
||||
self.player_location_table.update(grass_location_name_to_id)
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
|
||||
@@ -245,6 +275,14 @@ class TunicWorld(World):
|
||||
self.get_location("Secret Gathering Place - 10 Fairy Reward").place_locked_item(laurels)
|
||||
items_to_create["Hero's Laurels"] = 0
|
||||
|
||||
if self.options.grass_randomizer:
|
||||
items_to_create["Grass"] = len(grass_location_table)
|
||||
tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression))
|
||||
items_to_create["Glass Cannon"] = 0
|
||||
for grass_location in excluded_grass_locations:
|
||||
self.get_location(grass_location).place_locked_item(self.create_item("Grass"))
|
||||
items_to_create["Grass"] -= len(excluded_grass_locations)
|
||||
|
||||
if self.options.keys_behind_bosses:
|
||||
for rgb_hexagon, location in hexagon_locations.items():
|
||||
hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
|
||||
@@ -332,8 +370,73 @@ class TunicWorld(World):
|
||||
if tunic_item.name in slot_data_item_names:
|
||||
self.slot_data_items.append(tunic_item)
|
||||
|
||||
# pull out the filler so that we can place it manually during pre_fill
|
||||
self.fill_items = []
|
||||
if self.options.local_fill > 0 and self.multiworld.players > 1:
|
||||
# skip items marked local or non-local, let fill deal with them in its own way
|
||||
# discard grass from non_local if it's meant to be limited
|
||||
if self.settings.limit_grass_rando:
|
||||
self.options.non_local_items.value.discard("Grass")
|
||||
all_filler: List[TunicItem] = []
|
||||
non_filler: List[TunicItem] = []
|
||||
for tunic_item in tunic_items:
|
||||
if (tunic_item.excludable
|
||||
and tunic_item.name not in self.options.local_items
|
||||
and tunic_item.name not in self.options.non_local_items):
|
||||
all_filler.append(tunic_item)
|
||||
else:
|
||||
non_filler.append(tunic_item)
|
||||
self.amount_to_local_fill = int(self.options.local_fill.value * len(all_filler) / 100)
|
||||
self.fill_items += all_filler[:self.amount_to_local_fill]
|
||||
del all_filler[:self.amount_to_local_fill]
|
||||
tunic_items = all_filler + non_filler
|
||||
|
||||
self.multiworld.itempool += tunic_items
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
self.fill_locations = []
|
||||
|
||||
if self.options.local_fill > 0 and self.multiworld.players > 1:
|
||||
# we need to reserve a couple locations so that we don't fill up every sphere 1 location
|
||||
reserved_locations: Set[str] = set(self.random.sample(sphere_one, 2))
|
||||
viable_locations = [loc for loc in self.multiworld.get_unfilled_locations(self.player)
|
||||
if loc.name not in reserved_locations
|
||||
and loc.name not in self.options.priority_locations.value]
|
||||
|
||||
if len(viable_locations) < self.amount_to_local_fill:
|
||||
raise OptionError(f"TUNIC: Not enough locations for local_fill option for {self.player_name}. "
|
||||
f"This is likely due to excess plando or priority locations.")
|
||||
|
||||
self.fill_locations += viable_locations
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
|
||||
tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
|
||||
if world.options.local_fill.value > 0]
|
||||
if tunic_fill_worlds:
|
||||
grass_fill: List[TunicItem] = []
|
||||
non_grass_fill: List[TunicItem] = []
|
||||
grass_fill_locations: List[Location] = []
|
||||
non_grass_fill_locations: List[Location] = []
|
||||
for world in tunic_fill_worlds:
|
||||
if world.options.grass_randomizer:
|
||||
grass_fill.extend(world.fill_items)
|
||||
grass_fill_locations.extend(world.fill_locations)
|
||||
else:
|
||||
non_grass_fill.extend(world.fill_items)
|
||||
non_grass_fill_locations.extend(world.fill_locations)
|
||||
|
||||
multiworld.random.shuffle(grass_fill)
|
||||
multiworld.random.shuffle(non_grass_fill)
|
||||
multiworld.random.shuffle(grass_fill_locations)
|
||||
multiworld.random.shuffle(non_grass_fill_locations)
|
||||
|
||||
for filler_item in grass_fill:
|
||||
multiworld.push_item(grass_fill_locations.pop(), filler_item, collect=False)
|
||||
|
||||
for filler_item in non_grass_fill:
|
||||
multiworld.push_item(non_grass_fill_locations.pop(), filler_item, collect=False)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
self.tunic_portal_pairs = {}
|
||||
self.er_portal_hints = {}
|
||||
@@ -346,7 +449,8 @@ class TunicWorld(World):
|
||||
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
|
||||
|
||||
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
|
||||
or self.options.grass_randomizer):
|
||||
portal_pairs = create_er_regions(self)
|
||||
if self.options.entrance_rando:
|
||||
# these get interpreted by the game to tell it which entrances to connect
|
||||
@@ -362,7 +466,7 @@ class TunicWorld(World):
|
||||
region = self.get_region(region_name)
|
||||
region.add_exits(exits)
|
||||
|
||||
for location_name, location_id in self.location_name_to_id.items():
|
||||
for location_name, location_id in self.player_location_table.items():
|
||||
region = self.get_region(location_table[location_name].region)
|
||||
location = TunicLocation(self.player, location_name, location_id, region)
|
||||
region.locations.append(location)
|
||||
@@ -375,7 +479,8 @@ class TunicWorld(World):
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# same reason as in create_regions, could probably be put into create_regions
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
|
||||
or self.options.grass_randomizer):
|
||||
set_er_location_rules(self)
|
||||
else:
|
||||
set_region_rules(self)
|
||||
@@ -463,6 +568,7 @@ class TunicWorld(World):
|
||||
"maskless": self.options.maskless.value,
|
||||
"entrance_rando": int(bool(self.options.entrance_rando.value)),
|
||||
"shuffle_ladders": self.options.shuffle_ladders.value,
|
||||
"grass_randomizer": self.options.grass_randomizer.value,
|
||||
"combat_logic": self.options.combat_logic.value,
|
||||
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
|
||||
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
|
||||
|
||||
@@ -629,14 +629,16 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
"Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests
|
||||
"West Garden before Terry": RegionInfo("Archipelagos Redux"), # the lower entry point, near hero grave
|
||||
"West Garden after Terry": RegionInfo("Archipelagos Redux"), # after Terry, up until next chompignons
|
||||
"West Garden West Combat": RegionInfo("Archipelagos Redux"), # for grass rando basically
|
||||
"West Garden at Dagger House": RegionInfo("Archipelagos Redux"), # just outside magic dagger house
|
||||
"West Garden South Checkpoint": RegionInfo("Archipelagos Redux"),
|
||||
"West Garden South Checkpoint": RegionInfo("Archipelagos Redux"), # the checkpoint and the blue lines area
|
||||
"Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats),
|
||||
"West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"),
|
||||
"West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted,
|
||||
outlet_region="West Garden by Portal"),
|
||||
"West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
|
||||
"West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
|
||||
"West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"),
|
||||
"West Garden before Boss": RegionInfo("Archipelagos Redux"), # main west garden
|
||||
"West Garden before Boss": RegionInfo("Archipelagos Redux"), # up the ladder before garden knight
|
||||
"West Garden after Boss": RegionInfo("Archipelagos Redux"),
|
||||
"West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden before Terry"),
|
||||
"Ruined Atoll": RegionInfo("Atoll Redux"),
|
||||
@@ -1165,8 +1167,10 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"West Garden after Terry": {
|
||||
"West Garden before Terry":
|
||||
[],
|
||||
"West Garden South Checkpoint":
|
||||
"West Garden West Combat":
|
||||
[],
|
||||
"West Garden South Checkpoint":
|
||||
[["Hyperdash"]],
|
||||
"West Garden Laurels Exit Region":
|
||||
[["LS1"]],
|
||||
},
|
||||
@@ -1176,6 +1180,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"West Garden at Dagger House":
|
||||
[],
|
||||
"West Garden after Terry":
|
||||
[["Hyperdash"]],
|
||||
"West Garden West Combat":
|
||||
[],
|
||||
},
|
||||
"West Garden before Boss": {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item
|
||||
from BaseClasses import Region, CollectionState
|
||||
from .options import IceGrappling, LadderStorage, CombatLogic
|
||||
from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
|
||||
laurels_zip, bomb_walls)
|
||||
from .er_data import Portal, get_portal_outlet_region
|
||||
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
|
||||
from .combat_logic import has_combat_reqs
|
||||
from BaseClasses import Region, CollectionState
|
||||
from .grass import set_grass_location_rules
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
@@ -555,7 +556,6 @@ 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)
|
||||
@@ -577,11 +577,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
wg_after_to_before_terry = regions["West Garden after Terry"].connect(
|
||||
connecting_region=regions["West Garden before Terry"])
|
||||
|
||||
regions["West Garden after Terry"].connect(
|
||||
connecting_region=regions["West Garden South Checkpoint"])
|
||||
wg_checkpoint_to_after_terry = regions["West Garden South Checkpoint"].connect(
|
||||
wg_after_terry_to_west_combat = regions["West Garden after Terry"].connect(
|
||||
connecting_region=regions["West Garden West Combat"])
|
||||
regions["West Garden West Combat"].connect(
|
||||
connecting_region=regions["West Garden after Terry"])
|
||||
|
||||
wg_checkpoint_to_west_combat = regions["West Garden South Checkpoint"].connect(
|
||||
connecting_region=regions["West Garden West Combat"])
|
||||
regions["West Garden West Combat"].connect(
|
||||
connecting_region=regions["West Garden South Checkpoint"])
|
||||
|
||||
# if not laurels, it goes through the west combat region instead
|
||||
regions["West Garden after Terry"].connect(
|
||||
connecting_region=regions["West Garden South Checkpoint"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["West Garden South Checkpoint"].connect(
|
||||
connecting_region=regions["West Garden after Terry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
wg_checkpoint_to_dagger = regions["West Garden South Checkpoint"].connect(
|
||||
connecting_region=regions["West Garden at Dagger House"])
|
||||
regions["West Garden at Dagger House"].connect(
|
||||
@@ -1402,11 +1415,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
set_rule(wg_after_to_before_terry,
|
||||
lambda state: state.has_any({laurels, ice_dagger}, player)
|
||||
or has_combat_reqs("West Garden", state, player))
|
||||
# laurels through, probably to the checkpoint, or just fight
|
||||
set_rule(wg_checkpoint_to_after_terry,
|
||||
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player))
|
||||
set_rule(wg_checkpoint_to_before_boss,
|
||||
|
||||
set_rule(wg_after_terry_to_west_combat,
|
||||
lambda state: has_combat_reqs("West Garden", state, player))
|
||||
set_rule(wg_checkpoint_to_west_combat,
|
||||
lambda state: has_combat_reqs("West Garden", state, player))
|
||||
|
||||
# maybe a little too generous? probably fine though
|
||||
set_rule(wg_checkpoint_to_before_boss,
|
||||
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player))
|
||||
|
||||
add_rule(btv_front_to_main,
|
||||
lambda state: has_combat_reqs("Beneath the Vault", state, player))
|
||||
@@ -1528,6 +1545,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
player = world.player
|
||||
|
||||
if world.options.grass_randomizer:
|
||||
set_grass_location_rules(world)
|
||||
|
||||
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
|
||||
|
||||
# Ability Shuffle Exclusive Rules
|
||||
@@ -1852,6 +1872,8 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Faeries", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Save Point", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [West Highlands] Upper Left Walkway", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [Central Highlands] Holy Cross (Blue Lines)", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [Central Highlands] Behind Guard Captain", "West Garden")
|
||||
|
||||
# with combat logic on, I presume the player will want to be able to see to avoid the spiders
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
|
||||
from BaseClasses import Region, ItemClassification, Item, Location
|
||||
from .locations import location_table
|
||||
from .locations import all_locations
|
||||
from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo
|
||||
from .er_rules import set_er_region_rules
|
||||
from Options import PlandoConnection
|
||||
@@ -53,8 +53,8 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
|
||||
set_er_region_rules(world, regions, portal_pairs)
|
||||
|
||||
for location_name, location_id in world.location_name_to_id.items():
|
||||
region = regions[location_table[location_name].er_region]
|
||||
for location_name, location_id in world.player_location_table.items():
|
||||
region = regions[all_locations[location_name].er_region]
|
||||
location = TunicERLocation(world.player, location_name, location_id, region)
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
7944
worlds/tunic/grass.py
Normal file
7944
worlds/tunic/grass.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -166,6 +166,7 @@ item_table: Dict[str, TunicItemData] = {
|
||||
"Ladders in Library": TunicItemData(IC.progression, 0, 148, "Ladders"),
|
||||
"Ladders in Lower Quarry": TunicItemData(IC.progression, 0, 149, "Ladders"),
|
||||
"Ladders in Swamp": TunicItemData(IC.progression, 0, 150, "Ladders"),
|
||||
"Grass": TunicItemData(IC.filler, 0, 151),
|
||||
}
|
||||
|
||||
# items to be replaced by fool traps
|
||||
@@ -214,7 +215,7 @@ combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's La
|
||||
|
||||
item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
|
||||
|
||||
filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler]
|
||||
filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler and name != "Grass"]
|
||||
|
||||
|
||||
def get_item_group(item_name: str) -> str:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, NamedTuple, Set, Optional
|
||||
from typing import Dict, NamedTuple, Set, Optional, List
|
||||
from .grass import grass_location_table
|
||||
|
||||
|
||||
class TunicLocationData(NamedTuple):
|
||||
@@ -320,7 +321,27 @@ hexagon_locations: Dict[str, str] = {
|
||||
"Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue",
|
||||
}
|
||||
|
||||
location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)}
|
||||
sphere_one: List[str] = [
|
||||
"Overworld - [Central] Chest Across From Well",
|
||||
"Overworld - [Northwest] Chest Near Quarry Gate",
|
||||
"Overworld - [Northwest] Shadowy Corner Chest",
|
||||
"Overworld - [Southwest] Chest Guarded By Turret",
|
||||
"Overworld - [Southwest] South Chest Near Guard",
|
||||
"Overworld - [Southwest] Obscured in Tunnel to Beach",
|
||||
"Overworld - [Northwest] Chest Near Turret",
|
||||
"Overworld - [Northwest] Page By Well",
|
||||
"Overworld - [West] Chest Behind Moss Wall",
|
||||
"Overworld - [Southwest] Key Pickup",
|
||||
"Overworld - [West] Key Pickup",
|
||||
"Overworld - [West] Obscured Behind Windmill",
|
||||
"Overworld - [West] Obscured Near Well",
|
||||
"Overworld - [West] Page On Teleporter"
|
||||
]
|
||||
|
||||
standard_location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)}
|
||||
|
||||
all_locations = location_table.copy()
|
||||
all_locations.update(grass_location_table)
|
||||
|
||||
location_name_groups: Dict[str, Set[str]] = {}
|
||||
for loc_name, loc_data in location_table.items():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any
|
||||
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
|
||||
PerGameCommonOptions, OptionGroup, Visibility)
|
||||
PerGameCommonOptions, OptionGroup, Visibility, NamedRange)
|
||||
from .er_data import portal_mapping
|
||||
|
||||
|
||||
@@ -154,6 +154,33 @@ class ShuffleLadders(Toggle):
|
||||
display_name = "Shuffle Ladders"
|
||||
|
||||
|
||||
class GrassRandomizer(Toggle):
|
||||
"""
|
||||
Turns over 6,000 blades of grass and bushes in the game into checks.
|
||||
"""
|
||||
internal_name = "grass_randomizer"
|
||||
display_name = "Grass Randomizer"
|
||||
|
||||
|
||||
class LocalFill(NamedRange):
|
||||
"""
|
||||
Choose the percentage of your filler/trap items that will be kept local or distributed to other TUNIC players with this option enabled.
|
||||
If you have Grass Randomizer enabled, this option must be set to 95% or higher to avoid flooding the item pool. The host can remove this restriction by turning off the limit_grass_rando setting in host.yaml.
|
||||
This option defaults to 95% if you have Grass Randomizer enabled, and to 0% otherwise.
|
||||
This option ignores items placed in your local_items or non_local_items.
|
||||
This option does nothing in single player games.
|
||||
"""
|
||||
internal_name = "local_fill"
|
||||
display_name = "Local Fill Percent"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
special_range_names = {
|
||||
"default": -1
|
||||
}
|
||||
default = -1
|
||||
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
|
||||
|
||||
|
||||
class TunicPlandoConnections(PlandoConnections):
|
||||
"""
|
||||
Generic connection plando. Format is:
|
||||
@@ -278,12 +305,13 @@ class TunicOptions(PerGameCommonOptions):
|
||||
combat_logic: CombatLogic
|
||||
lanternless: Lanternless
|
||||
maskless: Maskless
|
||||
grass_randomizer: GrassRandomizer
|
||||
local_fill: LocalFill
|
||||
laurels_zips: LaurelsZips
|
||||
ice_grappling: IceGrappling
|
||||
ladder_storage: LadderStorage
|
||||
ladder_storage_without_items: LadderStorageWithoutItems
|
||||
plando_connections: TunicPlandoConnections
|
||||
|
||||
logic_rules: LogicRules
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user