Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 74 additions & 40 deletions EntranceShuffle.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions HintList.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ def rainbow_bridge_hint_kind(world: World) -> str:
'Magic Meter': (["mystic training", "pixie dust", "a green rectangle"], "a Magic Meter", 'item'),
'Double Defense': (["a white outline", "damage decrease", "strengthened love"], "Double Defense", 'item'),
'Slingshot': (["a seed shooter", "a rubberband", "a child's catapult"], "a Slingshot", 'item'),
'Deku Seed Bag': (["a seed shooter", "a rubberband", "a child's catapult"], "a Slingshot", 'item'),
'Boomerang': (["a banana", "a stun stick"], "the Boomerang", 'item'),
'Bow': (["an archery enabler", "a danger dart launcher"], "a Bow", 'item'),
'Bomb Bag': (["an explosive container", "a blast bag"], "a Bomb Bag", 'item'),
Expand Down
2 changes: 2 additions & 0 deletions ItemList.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ class GetItemId(IntEnum):
'Bomb Bag': ('Item', True, GetItemId.GI_PROGRESSIVE_BOMB_BAG, None),
'Bow': ('Item', True, GetItemId.GI_PROGRESSIVE_BOW, None),
'Slingshot': ('Item', True, GetItemId.GI_PROGRESSIVE_SLINGSHOT, None),
'Deku Seed Bag': ('Item', True, GetItemId.GI_PROGRESSIVE_SLINGSHOT, {'alias': ('Slingshot', 1)}),
'Progressive Wallet': ('Item', True, GetItemId.GI_PROGRESSIVE_WALLET, {'progressive': 3}),
'Progressive Scale': ('Item', True, GetItemId.GI_PROGRESSIVE_SCALE, {'progressive': 2}),
'Deku Nut Capacity': ('Item', None, GetItemId.GI_PROGRESSIVE_NUT_CAPACITY, None),
Expand Down Expand Up @@ -549,6 +550,7 @@ class GetItemId(IntEnum):

# Event items otherwise generated by generic event logic
# can be defined here to enforce their appearance in playthroughs.
'Deku Tree Clear': ('Event', True, None, None),
'Water Temple Clear': ('Event', True, None, None),
'Forest Trial Clear': ('Event', True, None, None),
'Fire Trial Clear': ('Event', True, None, None),
Expand Down
35 changes: 33 additions & 2 deletions ItemPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
from World import World


closed_forest_restricted_items: tuple[str, ...] = (
'Bomb Bag',
'Bombchus (5)',
'Bombchus (10)',
'Bombchus (20)',
'Bombchus',
'Dins Fire',
'Progressive Scale',
'Bolero of Fire',
'Serenade of Water',
'Nocturne of Shadow',
'Requiem of Spirit',
'Prelude of Light',
)

plentiful_items: list[str] = [
'Biggoron Sword',
'Boomerang',
Expand All @@ -34,7 +49,6 @@
'Deku Stick Capacity',
'Deku Nut Capacity',
'Bow',
'Slingshot',
'Bomb Bag',
'Double Defense',
]
Expand Down Expand Up @@ -64,7 +78,6 @@
'Progressive Wallet',
'Magic Meter',
'Bow',
'Slingshot',
'Bomb Bag',
'Bombchus (10)',
'Lens of Truth',
Expand Down Expand Up @@ -103,6 +116,7 @@
'Requiem of Spirit',
'Ocarina',
'Kokiri Sword',
'Deku Seed Bag',
'Boss Key (Ganons Castle)',
'Boss Key (Forest Temple)',
'Boss Key (Fire Temple)',
Expand Down Expand Up @@ -226,6 +240,7 @@
'Deku Nut Capacity': 1,
'Bow': 2,
'Slingshot': 2,
'Deku Seed Bag': 1,
'Bomb Bag': 2,
'Heart Container': 0,
},
Expand All @@ -240,6 +255,7 @@
'Deku Nut Capacity': 0,
'Bow': 1,
'Slingshot': 1,
'Deku Seed Bag': 0,
'Bomb Bag': 1,
'Heart Container': 0,
'Piece of Heart': 0,
Expand Down Expand Up @@ -467,6 +483,10 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:

if world.settings.item_pool_value == 'plentiful':
pending_junk_pool.extend(plentiful_items)
if world.settings.open_forest == 'closed' and world.settings.world_count > 1:
pending_junk_pool.append('Deku Seed Bag')
else:
pending_junk_pool.append('Slingshot')
if world.settings.shuffle_child_trade:
pending_junk_pool.extend(world.settings.shuffle_child_trade)
# Weird Egg is always chosen if both Egg and Chicken are selected to be shuffled.
Expand Down Expand Up @@ -822,6 +842,17 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
dungeon = [dungeon for dungeon in world.dungeons if dungeon.name == 'Ganons Castle'][0]
dungeon.boss_key.append(ItemFactory(item, world))

# Slingshots
elif location.vanilla_item == 'Deku Seed Bag':
# In multiworld with closed forest, one slingshot for each player is forced to be placed in the forest area,
# to reduce the likelihood that players are locked inside the forest area for a long time.
# We call the slingshots that can be placed freely “Deku seed bags” to allow the fill algorithm to distinguish between them.
# However, in other settings, this distinction is not necessary, so to make spoiler logs easier to, we call all slingshots slingshots.
# (To simplify the ludicrous implementation, we always use the Deku seed bag name there as well.)
if world.settings.item_pool_value != 'ludicrous' and (world.settings.open_forest != 'closed' or world.settings.world_count == 1):
item = 'Slingshot'
shuffle_item = True

# Dungeon Items
elif location.dungeon is not None:
dungeon = location.dungeon
Expand Down
4 changes: 2 additions & 2 deletions LocationList.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def shop_address(shop_id: int, shelf_id: int) -> int:
# Lost Woods
("LW Gift from Saria", ("Cutscene", 0xFF, 0x02, None, 'Ocarina', ("Lost Woods", "Forest Area", "NPCs",))),
("LW Ocarina Memory Game", ("NPC", 0x5B, 0x76, None, 'Piece of Heart', ("Lost Woods", "Forest Area", "Minigames",))),
("LW Target in Woods", ("NPC", 0x5B, 0x60, None, 'Slingshot', ("Lost Woods", "Forest Area", "NPCs",))),
("LW Target in Woods", ("NPC", 0x5B, 0x60, None, 'Deku Seed Bag', ("Lost Woods", "Forest Area", "NPCs",))),
("LW Near Shortcuts Grotto Chest", ("Chest", 0x3E, 0x14, None, 'Rupees (5)', ("Lost Woods", "Forest Area", "Grottos", "Chests",))),
("LW Trade Cojiro", ("NPC", 0x5B, 0x1F, None, 'Odd Mushroom', ("Lost Woods", "Forest",))),
("LW Trade Odd Potion", ("NPC", 0x5B, 0x21, None, 'Poachers Saw', ("Lost Woods", "Forest",))),
Expand Down Expand Up @@ -248,7 +248,7 @@ def shop_address(shop_id: int, shelf_id: int) -> int:
("HF Child Above Drawbridge Wonderitem 2", ("Wonderitem", 0x51, [(0,0,54),(0,1,52)], None, 'Rupees (20)', ("Hyrule Field", "Wonderitem"))),
("HF Child Above Drawbridge Wonderitem 3", ("Wonderitem", 0x51, [(0,0,55),(0,1,53)], None, 'Rupees (20)', ("Hyrule Field", "Wonderitem"))),

("Market Shooting Gallery Reward", ("NPC", 0x42, 0x60, None, 'Slingshot', ("Market", "Minigames",))),
("Market Shooting Gallery Reward", ("NPC", 0x42, 0x60, None, 'Deku Seed Bag', ("Market", "Minigames",))),
("Market Bombchu Bowling First Prize", ("NPC", 0x4B, 0x34, None, 'Bomb Bag', ("Market", "Minigames",))),
("Market Bombchu Bowling Second Prize", ("NPC", 0x4B, 0x3E, None, 'Piece of Heart', ("Market", "Minigames",))),
("Market Bombchu Bowling Bombchus", ("NPC", 0x4B, 0x03, None, 'Bombchus (10)', None)),
Expand Down
13 changes: 13 additions & 0 deletions Region.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def alt_hint(self) -> Optional[HintArea]:

def can_fill(self, item: Item, manual: bool = False) -> bool:
from Hints import HintArea
from ItemPool import closed_forest_restricted_items

if not manual and self.world.settings.empty_dungeons_mode != 'none' and item.dungeonitem:
# An empty dungeon can only store its own dungeon items
Expand All @@ -103,6 +104,18 @@ def can_fill(self, item: Item, manual: bool = False) -> bool:
if item.world.empty_dungeons[dungeon.name].empty and dungeon.is_dungeon_item(item):
return False

if not manual and self.world.settings.open_forest == 'closed' and item.name in (*closed_forest_restricted_items, 'Slingshot'):
hint_area = HintArea.at(self)
if hint_area.color == 'Green' and hint_area != HintArea.FOREST_TEMPLE and self.name != 'Queen Gohma Boss Room':
# Don't place items that can be used to escape the forest in Forest areas of worlds with Require Gohma
if item.name in closed_forest_restricted_items:
return False
else:
# Place at least one slingshot for each player in the Forest area, to avoid requiring one player to leave the forest to get another player's slingshot.
# This is still not a 100% guarantee because the slingshot could be behind an item that's not in the forest, such as in a bombable grotto entrance in the Lost Woods.
if item.name == 'Slingshot':
return False

is_self_dungeon_restricted = False
is_self_region_restricted = None
is_hint_color_restricted = None
Expand Down
5 changes: 4 additions & 1 deletion Rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections.abc import Callable, Collection, Iterable
from typing import TYPE_CHECKING, Optional

from ItemPool import song_list
from ItemPool import closed_forest_restricted_items, song_list
from Location import Location, DisableType
from RulesCommon import AccessRule
from Search import Search
Expand Down Expand Up @@ -79,6 +79,9 @@ def set_rules(world: World) -> None:
if location.name in world.always_hints:
location.add_rule(guarantee_hint)

if world.settings.open_forest == 'closed' and location in world.distribution.skipped_locations:
add_item_rule(location, lambda location, item: item.name not in closed_forest_restricted_items)

for location in world.settings.disabled_locations:
try:
world.get_location(location).disabled = DisableType.PENDING
Expand Down
4 changes: 4 additions & 0 deletions SaveContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,10 @@ def get_save_context_addresses() -> AddressesDict:
'item_slot.slingshot' : 'slingshot',
'upgrades.bullet_bag' : None,
},
"Deku Seed Bag" : {
'item_slot.slingshot' : 'slingshot',
'upgrades.bullet_bag' : None,
},
"Deku Seeds" : {
'ammo.slingshot' : None,
},
Expand Down
27 changes: 15 additions & 12 deletions SettingsList.py
Original file line number Diff line number Diff line change
Expand Up @@ -1235,21 +1235,24 @@ class SettingInfos:
Deku Tree, requiring Kokiri Sword and Deku Shield to access
the Deku Tree.

'Closed Forest': Beating Deku Tree is logically required
to leave the forest area (Kokiri Forest/Lost Woods/Sacred Forest
Meadow/Deku Tree), while the Kokiri Sword and a Deku Shield are
required to access the Deku Tree. Items needed for this will be
guaranteed inside the forest area. This setting is incompatible
with starting as adult, and so Starting Age will be locked to Child.
With either "Shuffle Interior Entrances" set to "All", "Shuffle
Overworld Entrances" on, "Randomize Warp Song Destinations" on
or "Randomize Overworld Spawns" on, Closed Forest will instead
be treated as Closed Deku with starting age Child and WILL NOT
guarantee that these items are available in the forest area.
'Closed Forest': Defeating Queen Gohma is required to leave
the forest area (Kokiri Forest/Lost Woods/Sacred Forest
Meadow/Deku Tree). Items needed for this will be guaranteed
inside the forest area, and items that could be used to
escape the forest without defeating Queen Gohma (such as
explosives to enter Goron City) will be prevented from
appearing inside the forest area. This setting is
incompatible with starting as adult, and so Starting Age will
be locked to Child. If entrances are shuffled, entrances
inside and outside the forest area will be shuffled
separately. For example, "Shuffle Dungeon Entrances" and
"Shuffle Boss Entrances" don't affect the Deku Tree. As an
exception, grottos are not shuffled separately, and neither
are interiors if only simple interiors are shuffled.
''',
shared = True,
disable = {
'closed': {'settings': ['starting_age']}
'closed': {'settings': ['starting_age']},
},
gui_params = {
'randomize_key': 'randomize_settings',
Expand Down
5 changes: 0 additions & 5 deletions World.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,6 @@ def __init__(self, world_id: int, settings: Settings, resolve_randomized_setting
self.selected_adult_trade_item = plando_adult_trade[0][1].item # ugly but functional, see the loop in Plandomizer.WorldDistribution.fill for how this is indexed
self.adult_trade_starting_inventory: str = ''

if (settings.open_forest == 'closed'
and (self.shuffle_special_interior_entrances or settings.shuffle_hideout_entrances or settings.shuffle_overworld_entrances
or settings.warp_songs or settings.spawn_positions)):
self.settings.open_forest = 'closed_deku'

if settings.triforce_goal_per_world > settings.triforce_count_per_world:
raise ValueError("Triforces required cannot be more than the triforce count.")
self.triforce_goal: int = settings.triforce_goal_per_world * settings.world_count
Expand Down
8 changes: 5 additions & 3 deletions data/Glitched World/Deku Tree MQ.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@
"region_name": "Deku Tree Boss Room",
"dungeon": "Deku Tree",
"events": {
"Deku Tree Clear": "Deku_Shield and (Kokiri_Sword or Sticks)"
"Defeat Queen Gohma": "Deku_Shield and (Kokiri_Sword or Sticks)",
# separate event which appears in the playthrough only if it's required to open the forest exit
"Deku Tree Clear": "'Defeat Queen Gohma'"
},
"locations": {
"Deku Tree MQ Before Spinning Log Chest": "True",
"Deku Tree MQ After Spinning Log Chest": "can_play(Song_of_Time)",
"Deku Tree MQ GS Basement Graves Room": "Boomerang and can_play(Song_of_Time)",
"Deku Tree MQ GS Basement Back Room": "Boomerang",
"Deku Tree MQ Deku Scrub": "True",
"Deku Tree Queen Gohma Heart": "Deku_Shield and (Kokiri_Sword or Sticks)",
"Queen Gohma": "Deku_Shield and (Kokiri_Sword or Sticks)"
"Deku Tree Queen Gohma Heart": "'Defeat Queen Gohma'",
"Queen Gohma": "'Defeat Queen Gohma'"
},
"exits": {
"Deku Tree Lobby": "True"
Expand Down
4 changes: 3 additions & 1 deletion data/Glitched World/Deku Tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"dungeon": "Deku Tree",
"events": {
"Defeat Queen Gohma": "(Nuts or can_use(Slingshot) or has_bombchus or can_use(Hookshot) or can_use(Bow) or can_use(Boomerang)) and
((here(has_shield or can_use(Megaton_Hammer)) and (is_adult or Kokiri_Sword or Sticks)) or is_adult)"
((here(has_shield or can_use(Megaton_Hammer)) and (is_adult or Kokiri_Sword or Sticks)) or is_adult)",
# separate event which appears in the playthrough only if it's required to open the forest exit
"Deku Tree Clear": "'Defeat Queen Gohma'"
},
"locations": {
"Deku Tree Queen Gohma Heart": "'Defeat Queen Gohma'",
Expand Down
2 changes: 1 addition & 1 deletion data/LogicHelpers.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"can_child_damage": "is_child and (Slingshot or Sticks or Kokiri_Sword or has_explosives or can_use(Dins_Fire))",
"can_cut_shrubs": "is_adult or Sticks or Kokiri_Sword or Boomerang or has_explosives",
"can_dive": "Progressive_Scale",
"can_leave_forest": "open_forest != 'closed' or is_adult or is_glitched or 'Defeat Queen Gohma'",
"can_leave_forest": "open_forest != 'closed' or is_adult or is_glitched or 'Deku Tree Clear'",
"can_plant_bugs": "is_child and Bugs",
"can_ride_epona": "is_adult and Epona and (can_play(Eponas_Song) or (is_glitched and can_hover))",
"can_stun_deku": "is_adult or (Slingshot or Boomerang or Sticks or Kokiri_Sword or has_explosives or can_use(Dins_Fire) or Nuts or Deku_Shield)",
Expand Down
4 changes: 3 additions & 1 deletion data/World/Bosses.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"scene": "Deku Tree Boss",
"is_boss_room": true,
"events": {
"Defeat Queen Gohma": "(Nuts or can_use(Slingshot)) and can_jumpslash"
"Defeat Queen Gohma": "(Nuts or can_use(Slingshot)) and can_jumpslash",
# separate event which appears in the playthrough only if it's required to open the forest exit
"Deku Tree Clear": "'Defeat Queen Gohma'"
},
"locations": {
"Deku Tree Queen Gohma Heart": "'Defeat Queen Gohma'",
Expand Down