Skip to content

Commit bafd242

Browse files
committed
Merge branch 'Dev' into dev-fenhl
# Conflicts: # Plandomizer.py # World.py # data/presets_default.json
2 parents 22a4b1f + 835f8ca commit bafd242

File tree

13 files changed

+221
-76
lines changed

13 files changed

+221
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Glitched logic has been renamed to `Advanced` and been revamped to work with any settings, including Entrance Randomization.
1010
* A slew of advanced tricks and glitches have been added for the new `Advanced` logic setting.
1111
* The `Open Door of Time` setting has been renamed to `Door of Time`, with new options to require the Spiritual Stones and/or the Ocarina of Time.
12+
* New `Additional Random Starting Items` setting.
1213

1314
## Bug fixes
1415
* The Deku Shield pot in the Spirit Temple is no longer shuffled when both `Fix Broken Drops` and `Include Empty Pots` are off.

ItemPool.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,14 @@
345345
"Mask of Truth",
346346
)
347347

348+
ocarina_buttons: tuple[str, ...] = (
349+
'Ocarina A Button',
350+
'Ocarina C down Button',
351+
'Ocarina C right Button',
352+
'Ocarina C left Button',
353+
'Ocarina C up Button',
354+
)
355+
348356
normal_bottles: list[str] = [bottle for bottle in sorted(ItemInfo.bottles) if bottle not in ('Deliver Letter', 'Sell Big Poe')] + ['Bottle with Big Poe']
349357
reward_list: list[str] = [item.name for item in sorted([i for n, i in ItemInfo.items.items() if i.type == 'DungeonReward'], key=lambda x: x.special['item_id'])]
350358
song_list: list[str] = [item.name for item in sorted([i for n, i in ItemInfo.items.items() if i.type == 'Song'], key=lambda x: x.index if x.index is not None else 0)]
@@ -572,7 +580,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
572580
if world.settings.shuffle_song_items == 'any':
573581
pending_junk_pool.extend(song_list)
574582
if world.settings.shuffle_individual_ocarina_notes:
575-
pending_junk_pool.extend(['Ocarina A Button', 'Ocarina C up Button', 'Ocarina C left Button', 'Ocarina C down Button', 'Ocarina C right Button'])
583+
pending_junk_pool.extend(ocarina_buttons)
576584

577585
if world.settings.triforce_hunt:
578586
if world.settings.triforce_hunt_mode == 'normal':
@@ -584,11 +592,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
584592
pending_junk_pool.extend(triforce_blitz_items)
585593
# Ice% is handled below
586594
if world.settings.shuffle_individual_ocarina_notes:
587-
pending_junk_pool.append('Ocarina A Button')
588-
pending_junk_pool.append('Ocarina C up Button')
589-
pending_junk_pool.append('Ocarina C left Button')
590-
pending_junk_pool.append('Ocarina C down Button')
591-
pending_junk_pool.append('Ocarina C right Button')
595+
pending_junk_pool.extend(ocarina_buttons)
592596

593597
# Use the vanilla items in the world's locations when appropriate.
594598
vanilla_items_processed = Counter()
@@ -1075,6 +1079,25 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
10751079
pool.remove(junk_item)
10761080
pool.append(pending_item)
10771081

1082+
world.distribution.collect_starters(world.state)
1083+
1084+
if not world.settings.shuffle_individual_ocarina_notes:
1085+
for ocarina_button in ocarina_buttons:
1086+
world.state.collect(ItemFactory(ocarina_button, world))
1087+
1088+
for _ in range(world.settings.add_random_starting_items):
1089+
random_starting_items_pool = sorted({item for item in pool if item not in ItemInfo.junk_weight}) # give each item the same weight regardless of how many copies there are
1090+
selected_item = random.choice(random_starting_items_pool)
1091+
world.randomized_starting_items[selected_item] = world.randomized_starting_items.get(selected_item, 0) + 1
1092+
pool.remove(selected_item)
1093+
pool.extend(get_junk_item())
1094+
for item, count in world.randomized_starting_items.items():
1095+
item = ItemFactory(item, world)
1096+
for _ in range(count):
1097+
if item.solver_id is not None:
1098+
world.state.collect(item)
1099+
world.distribution.randomized_starting_items = world.randomized_starting_items
1100+
10781101
if world.settings.junk_ice_traps in ('custom_count', 'custom_percent'):
10791102
junk_pool[:] = [('Ice Trap', 1)]
10801103
# Get a list of all "junk" type items
@@ -1138,13 +1161,4 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
11381161
item_groups['Junk'] = remove_junk_items
11391162
world.distribution.distribution.search_groups['Junk'] = remove_junk_items
11401163

1141-
world.distribution.collect_starters(world.state)
1142-
1143-
if not world.settings.shuffle_individual_ocarina_notes:
1144-
world.state.collect(ItemFactory('Ocarina A Button', world))
1145-
world.state.collect(ItemFactory('Ocarina C up Button', world))
1146-
world.state.collect(ItemFactory('Ocarina C down Button', world))
1147-
world.state.collect(ItemFactory('Ocarina C left Button', world))
1148-
world.state.collect(ItemFactory('Ocarina C right Button', world))
1149-
11501164
return pool, placed_items

Patches.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2218,6 +2218,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
22182218

22192219
# actually write the save table to rom
22202220
world.distribution.give_items(world, save_context)
2221+
world.distribution.give_randomized_items(world, save_context)
22212222
if world.settings.starting_age == 'adult':
22222223
# When starting as adult, the pedestal doesn't handle child default equips when going back child the first time, so we have to equip them ourselves
22232224
save_context.equip_default_items('child')

Plandomizer.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from Fill import FillError
1616
from Hints import HintArea, gossipLocations, GossipText, hint_func
1717
from Item import ItemFactory, ItemInfo, ItemIterator, is_item, Item
18-
from ItemPool import item_groups, get_junk_item, song_list, trade_items, child_trade_items, eggs, triforce_blitz_items, triforce_pieces
18+
from ItemPool import item_groups, get_junk_item, song_list, trade_items, child_trade_items, ocarina_buttons, eggs, triforce_blitz_items, triforce_pieces
1919
from JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
2020
from Location import Location, LocationIterator, LocationFactory
2121
from LocationList import location_groups, location_table
@@ -45,6 +45,7 @@ class InvalidFileException(Exception):
4545
'songs',
4646
'entrances',
4747
'locations',
48+
':randomized_starting_items',
4849
':skipped_locations',
4950
':woth_locations',
5051
':goal_locations',
@@ -272,6 +273,7 @@ def __init__(self, distribution: Distribution, id: int, src_dict: Optional[dict[
272273
self.item_pool: Optional[dict[str, ItemPoolRecord]] = None
273274
self.entrances: Optional[dict[str, EntranceRecord]] = None
274275
self.locations: Optional[dict[str, LocationRecord | list[LocationRecord]]] = None
276+
self.randomized_starting_items: Optional[dict[str, int]] = None
275277
self.woth_locations: Optional[dict[str, LocationRecord]] = None
276278
self.goal_locations: Optional[dict[str, dict[str, dict[str, LocationRecord | dict[str, LocationRecord]]]]] = None
277279
self.barren_regions: Optional[list[str]] = None
@@ -300,6 +302,7 @@ def update(self, src_dict: dict[str, Any], update_all: bool = False) -> None:
300302
'item_pool': {name: ItemPoolRecord(record) for (name, record) in src_dict.get('item_pool', {}).items()},
301303
'entrances': {name: EntranceRecord(record) for (name, record) in src_dict.get('entrances', {}).items()},
302304
'locations': {name: [LocationRecord(rec) for rec in record] if is_pattern(name) else LocationRecord(record) for (name, record) in src_dict.get('locations', {}).items() if not is_output_only(name)},
305+
'randomized_starting_items': None,
303306
'woth_locations': None,
304307
'goal_locations': None,
305308
'barren_regions': None,
@@ -332,6 +335,7 @@ def to_json(self) -> dict[str, Any]:
332335
'item_pool': SortedDict({name: record.to_json() for (name, record) in self.item_pool.items()}),
333336
'entrances': {name: record.to_json() for (name, record) in self.entrances.items()},
334337
'locations': {name: [rec.to_json() for rec in record] if is_pattern(name) else record.to_json() for (name, record) in self.locations.items()},
338+
':randomized_starting_items': None if self.randomized_starting_items is None else {name: count for (name, count) in self.randomized_starting_items.items()},
335339
':skipped_locations': {loc.name: LocationRecord.from_item(loc.item).to_json() for loc in self.skipped_locations},
336340
':woth_locations': None if self.woth_locations is None else {name: record.to_json() for (name, record) in self.woth_locations.items()},
337341
':goal_locations': self.goal_locations,
@@ -1077,6 +1081,10 @@ def give_items(self, world: World, save_context: SaveContext) -> None:
10771081
continue
10781082
save_context.give_item(world, name, record.count)
10791083

1084+
def give_randomized_items(self, world: World, save_context: SaveContext) -> None:
1085+
for item, count in world.randomized_starting_items.items():
1086+
save_context.give_item(world, item, count)
1087+
10801088
def get_starting_item(self, item: str) -> int:
10811089
items = self.settings.starting_items
10821090
if item in items:

SettingsList.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3834,6 +3834,18 @@ class SettingInfos:
38343834
''',
38353835
)
38363836

3837+
add_random_starting_items = Scale(
3838+
gui_text = 'Additional Random Starting Items',
3839+
gui_tooltip = '''\
3840+
Begin the game with this many randomly selected items in
3841+
addition to your selections from the tables.
3842+
''',
3843+
default = 0,
3844+
minimum = 0,
3845+
maximum = 10,
3846+
shared = True,
3847+
)
3848+
38373849
start_with_consumables = Checkbutton(
38383850
gui_text = 'Start with Consumables',
38393851
gui_tooltip = '''\

Unittest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ def test_should_not_throw_exception(self):
328328
"plando-potscrates-allmq",
329329
"plando-beehives",
330330
"plando-freestanding-pots-crates-beehives-triforcehunt",
331+
"random_starting_items",
331332
]
332333
for filename in filenames:
333334
with self.subTest(filename):

World.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(self, world_id: int, settings: Settings, resolve_randomized_setting
5858
self.barren_dungeon: int = 0
5959
self.woth_dungeon: int = 0
6060
self.randomized_list: list[str] = []
61+
self.randomized_starting_items: dict[str, int] = {}
6162
self.cached_bigocto_location: Optional[EllipsisType | Location] = ...
6263

6364
self.parser: Rule_AST_Transformer = Rule_AST_Transformer(self)
@@ -435,6 +436,7 @@ def copy(self) -> World:
435436
new_world.randomized_list = list(self.randomized_list)
436437
for randomized_item in new_world.randomized_list:
437438
setattr(new_world, randomized_item, getattr(self.settings, randomized_item))
439+
new_world.distribution.randomized_starting_items = new_world.randomized_starting_items = copy.copy(self.randomized_starting_items)
438440

439441
new_world.always_hints = list(self.always_hints)
440442
new_world.max_progressions = copy.copy(self.max_progressions)

data/Hints/scrubs.json

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
{
22
"name": "scrubs",
33
"gui_name": "Scrubs",
4-
"description": "Tournament hints used for Scrubs Races. Duplicates of each hint except foolish and item which are placed at ToT. Skull Mask, Ice Cavern Final Room and Castle Great Fairies are always hinted, 4 Path, 3 Foolish (no Dungeons), 1 Item, 9 sometimes, HC(Storms) & HF(Cow Grotto) junked.",
4+
"description": "Tournament hints used for Scrubs Races. Duplicates of each hint except foolish which are placed at ToT. Skull Mask, Ice Cavern Final Room, Frogs Rewards and Castle Great Fairies are always hinted, 5 Path, 4 Foolish (2 Dungeons), 8 sometimes (2 Dual), HC(Storms) & HF(Cow Grotto) junked.",
55
"add_locations": [
66
{ "location": "Castle Fairy Checks", "types": ["dual_always"] },
77
{ "location": "Ice Cavern Final Room", "types": ["dual_always"] },
8-
{ "location": "Deku Theater Skull Mask", "types": ["always"] }
8+
{ "location": "Deku Theater Skull Mask", "types": ["always"] },
9+
{ "location": "ZR Frogs Rewards", "types": ["dual_always"] }
910
],
1011
"remove_locations": [
12+
{ "location": "the Haunted Wasteland", "types": ["barren"] },
1113
{ "location": "Hyrule Castle", "types": ["barren"] },
12-
{ "location": "OGC Great Fairy Reward", "types": ["always"] },
13-
{ "location": "Sheik at Temple", "types": ["always", "sometimes"] },
14-
{ "location": "ZR Frogs Rewards", "types": ["dual_always", "dual"] },
14+
{ "location": "Gerudo's Fortress", "types": ["barren"] },
15+
{ "location": "the Temple of Time", "types": ["barren"] },
16+
{ "location": "ZR Frogs Ocarina Game", "types": ["always"] },
1517
{ "location": "Sheik in Forest", "types": ["sometimes"] },
1618
{ "location": "Sheik in Crater", "types": ["sometimes"] },
17-
{ "location": "Sheik in Kakariko", "types": ["always", "sometimes"] },
1819
{ "location": "Sheik in Ice Cavern", "types": ["sometimes"] },
1920
{ "location": "Sheik at Colossus", "types": ["sometimes"] },
2021
{ "location": "Song from Royal Familys Tomb", "types": ["sometimes"] },
21-
{ "location": "Song from Ocarina of Time", "types": ["always", "sometimes"] },
22-
{ "location": "Deku Theater Rewards", "types": ["dual_always"] }
22+
{ "location": "Deku Theater Rewards", "types": ["dual_always", "dual"] },
23+
{ "location": "Fire Temple Lower Loop", "types": ["dual"] },
24+
{ "location": "GF Horseback Archery Rewards", "types": ["dual_always", "dual"] },
25+
{ "location": "Kak Anju as Child", "types": ["sometimes"] }
2326
],
2427
"add_items": [],
2528
"remove_items": [
2629
{ "item": "Zeldas Lullaby", "types": ["woth", "goal"] },
2730
{ "item": "Nocturne of Shadow", "types": ["woth", "goal"] }
2831
],
29-
"dungeons_woth_limit": 2,
30-
"dungeons_barren_limit": 0,
32+
"dungeons_woth_limit": 5,
33+
"dungeons_barren_limit": 2,
3134
"named_items_required": true,
3235
"vague_named_items": false,
3336
"use_default_goals": true,
@@ -56,13 +59,13 @@
5659
"HC (Storms Grotto)",
5760
"HF (Cow Grotto)"
5861
]},
59-
"barren": {"order": 4, "weight": 0.0, "fixed": 3, "copies": 1, "priority_stones": [
62+
"barren": {"order": 4, "weight": 0.0, "fixed": 4, "copies": 1, "priority_stones": [
6063
"ToT (Left)",
6164
"ToT (Left-Center)",
6265
"ToT (Right)",
6366
"ToT (Right-Center)"
6467
]},
65-
"goal": {"order": 5, "weight": 0.0, "fixed": 4, "copies": 2, "remove_stones": [
68+
"goal": {"order": 5, "weight": 0.0, "fixed": 5, "copies": 2, "remove_stones": [
6669
"ToT (Left)",
6770
"ToT (Left-Center)",
6871
"ToT (Right)",
@@ -78,11 +81,13 @@
7881
"HC (Storms Grotto)",
7982
"HF (Cow Grotto)"
8083
]},
81-
"item": {"order": 7, "weight": 0.0, "fixed": 1, "copies": 1, "priority_stones": [
84+
"dual": {"order": 7, "weight": 0.0, "fixed": 2, "copies": 2, "remove_stones": [
8285
"ToT (Left)",
8386
"ToT (Left-Center)",
8487
"ToT (Right)",
85-
"ToT (Right-Center)"
88+
"ToT (Right-Center)",
89+
"HC (Storms Grotto)",
90+
"HF (Cow Grotto)"
8691
]},
8792
"junk": {"order": 8, "weight": 0.0, "fixed": 1, "copies": 2, "priority_stones": [
8893
"HC (Storms Grotto)",
@@ -104,7 +109,7 @@
104109
"song": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
105110
"overworld": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
106111
"dungeon": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
107-
"dual": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 0},
112+
"item": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 0},
108113
"named-item": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
109114
"entrance_always": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
110115
"important_check": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 0}

0 commit comments

Comments
 (0)