Skip to content

Commit 4de3236

Browse files
committed
Merge 'Generate fewer refill special deals above market price' (OoTRandomizer#2418)
# Conflicts: # ItemList.py
2 parents 9e07175 + 311bd21 commit 4de3236

5 files changed

Lines changed: 56 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* The Farore's Wind text box now distinguishes between Ganon's Castle and Ganon's Tower.
88
* New `Water Hop` trick added to advanced logic.
99
* Improve Debug menu with new options.
10+
* Refill items sold as special deals are now less likely to cost more than the "market price" of a repeatable purchase.
1011

1112
## Bug fixes
1213
* Fix a potential softlock when talking to Pierre (the upper scarecrow) as child in Lake Hylia.

Item.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def __init__(self, name: str = '', event: bool = False) -> None:
4848
self.junk: Optional[int] = self.special.get('junk', None)
4949
self.trade: bool = self.special.get('trade', False)
5050
self.ocarina_button: bool = self.special.get('ocarina_button', False)
51+
self.market_price: Optional[int] = self.special.get('market_price', None)
52+
self.market_price_non_chu_drops_only: bool = self.special.get('market_price_non_chu_drops_only', False)
5153

5254
self.solver_id: Optional[int] = None
5355
if name and self.junk is None:

ItemList.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,11 @@ class GetItemId(IntEnum):
331331
# of that item.
332332
#
333333
item_table: dict[str, tuple[str, Optional[bool], Optional[int], Optional[dict[str, Any]]]] = {
334-
'Bombs (5)': ('Item', None, GetItemId.GI_BOMBS_5, {'junk': 8}),
335-
'Deku Nuts (5)': ('Item', None, GetItemId.GI_DEKU_NUTS_5, {'junk': 5}),
336-
'Bombchus (10)': ('Item', True, GetItemId.GI_BOMBCHUS_10, None),
334+
'Bombs (5)': ('Item', None, GetItemId.GI_BOMBS_5, {'junk': 8, 'market_price': 25}),
335+
'Deku Nuts (5)': ('Item', None, GetItemId.GI_DEKU_NUTS_5, {'junk': 5, 'market_price': 15}),
336+
'Bombchus (10)': ('Item', True, GetItemId.GI_BOMBCHUS_10, {'market_price': 99, 'market_price_non_chu_drops_only': True}),
337337
'Boomerang': ('Item', True, GetItemId.GI_BOOMERANG, None),
338-
'Deku Stick (1)': ('Item', None, GetItemId.GI_DEKU_STICKS_1, {'junk': 5}),
338+
'Deku Stick (1)': ('Item', None, GetItemId.GI_DEKU_STICKS_1, {'junk': 5, 'market_price': 10}),
339339
'Lens of Truth': ('Item', True, GetItemId.GI_LENS_OF_TRUTH, None),
340340
'Megaton Hammer': ('Item', True, GetItemId.GI_HAMMER, None),
341341
'Cojiro': ('Item', True, GetItemId.GI_COJIRO, {'trade': True}),
@@ -364,8 +364,8 @@ class GetItemId(IntEnum):
364364
'Claim Check': ('Item', True, GetItemId.GI_CLAIM_CHECK, {'trade': True}),
365365
'Kokiri Sword': ('Item', True, GetItemId.GI_SWORD_KOKIRI, None),
366366
'Giants Knife': ('Item', True, GetItemId.GI_SWORD_KNIFE, None),
367-
'Deku Shield': ('Item', None, GetItemId.GI_SHIELD_DEKU, None),
368-
'Hylian Shield': ('Item', None, GetItemId.GI_SHIELD_HYLIAN, None),
367+
'Deku Shield': ('Item', None, GetItemId.GI_SHIELD_DEKU, {'market_price': 40}),
368+
'Hylian Shield': ('Item', None, GetItemId.GI_SHIELD_HYLIAN, {'market_price': 80}),
369369
'Mirror Shield': ('Item', True, GetItemId.GI_SHIELD_MIRROR, None),
370370
'Goron Tunic': ('Item', True, GetItemId.GI_TUNIC_GORON, None),
371371
'Zora Tunic': ('Item', True, GetItemId.GI_TUNIC_ZORA, None),
@@ -381,19 +381,19 @@ class GetItemId(IntEnum):
381381
'Map': ('Map', None, GetItemId.GI_DUNGEON_MAP, None),
382382
'Small Key': ('SmallKey', True, GetItemId.GI_SMALL_KEY, {'progressive': float('Inf')}),
383383
'Weird Egg': ('Item', True, GetItemId.GI_WEIRD_EGG, {'trade': True}),
384-
'Recovery Heart': ('Item', None, GetItemId.GI_RECOVERY_HEART, {'junk': 0}),
384+
'Recovery Heart': ('Item', None, GetItemId.GI_RECOVERY_HEART, {'junk': 0, 'market_price': 10}),
385385
'Arrows (5)': ('Item', None, GetItemId.GI_ARROWS_5, {'junk': 8}),
386-
'Arrows (10)': ('Item', None, GetItemId.GI_ARROWS_10, {'junk': 2}),
387-
'Arrows (30)': ('Item', None, GetItemId.GI_ARROWS_30, {'junk': 0}),
388-
'Rupee (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN, {'junk': -1}),
389-
'Rupees (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE, {'junk': 10}),
390-
'Rupees (20)': ('Item', None, GetItemId.GI_RUPEE_RED, {'junk': 4}),
386+
'Arrows (10)': ('Item', None, GetItemId.GI_ARROWS_10, {'junk': 2, 'market_price': 20}),
387+
'Arrows (30)': ('Item', None, GetItemId.GI_ARROWS_30, {'junk': 0, 'market_price': 60}),
388+
'Rupee (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN, {'junk': -1, 'market_price': 1}),
389+
'Rupees (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE, {'junk': 10, 'market_price': 5}),
390+
'Rupees (20)': ('Item', None, GetItemId.GI_RUPEE_RED, {'junk': 4, 'market_price': 20}),
391391
'Milk': ('Item', None, GetItemId.GI_MILK, None),
392392
'Goron Mask': ('Item', None, GetItemId.GI_MASK_GORON, {'trade': True, 'object': 0x0150}),
393393
'Zora Mask': ('Item', None, GetItemId.GI_MASK_ZORA, {'trade': True, 'object': 0x0151}),
394394
'Gerudo Mask': ('Item', None, GetItemId.GI_MASK_GERUDO, {'trade': True, 'object': 0x0152}),
395-
'Rupees (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE, {'junk': 1}),
396-
'Rupees (200)': ('Item', None, GetItemId.GI_RUPEE_GOLD, {'junk': 0}),
395+
'Rupees (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE, {'junk': 1, 'market_price': 50}),
396+
'Rupees (200)': ('Item', None, GetItemId.GI_RUPEE_GOLD, {'junk': 0, 'market_price': 200}),
397397
'Biggoron Sword': ('Item', True, GetItemId.GI_SWORD_BIGGORON, None),
398398
'Fire Arrows': ('Item', True, GetItemId.GI_ARROW_FIRE, None),
399399
'Ice Arrows': ('Item', True, GetItemId.GI_ARROW_ICE, None),
@@ -403,18 +403,18 @@ class GetItemId(IntEnum):
403403
'Dins Fire': ('Item', True, GetItemId.GI_DINS_FIRE, None),
404404
'Farores Wind': ('Item', True, GetItemId.GI_FARORES_WIND, None),
405405
'Nayrus Love': ('Item', True, GetItemId.GI_NAYRUS_LOVE, None),
406-
'Deku Nuts (10)': ('Item', None, GetItemId.GI_DEKU_NUTS_10, {'junk': 0}),
406+
'Deku Nuts (10)': ('Item', None, GetItemId.GI_DEKU_NUTS_10, {'junk': 0, 'market_price': 30}),
407407
'Bomb (1)': ('Item', None, GetItemId.GI_BOMBS_1, {'junk': -1}),
408-
'Bombs (10)': ('Item', None, GetItemId.GI_BOMBS_10, {'junk': 2}),
409-
'Bombs (20)': ('Item', None, GetItemId.GI_BOMBS_20, {'junk': 0}),
410-
'Deku Seeds (30)': ('Item', None, GetItemId.GI_DEKU_SEEDS_30, {'junk': 5}),
411-
'Bombchus (5)': ('Item', True, GetItemId.GI_BOMBCHUS_5, None),
412-
'Bombchus (20)': ('Item', True, GetItemId.GI_BOMBCHUS_20, None),
408+
'Bombs (10)': ('Item', None, GetItemId.GI_BOMBS_10, {'junk': 2, 'market_price': 50}),
409+
'Bombs (20)': ('Item', None, GetItemId.GI_BOMBS_20, {'junk': 0, 'market_price': 80}),
410+
'Deku Seeds (30)': ('Item', None, GetItemId.GI_DEKU_SEEDS_30, {'junk': 5, 'market_price': 30}),
411+
'Bombchus (5)': ('Item', True, GetItemId.GI_BOMBCHUS_5, {'market_price': 60, 'market_price_non_chu_drops_only': True}),
412+
'Bombchus (20)': ('Item', True, GetItemId.GI_BOMBCHUS_20, {'market_price': 180, 'market_price_non_chu_drops_only': True}),
413413
'Small Key (Treasure Chest Game)': ('TCGSmallKey', True, GetItemId.GI_DOOR_KEY, {'progressive': float('Inf')}),
414-
'Rupee (Treasure Chest Game) (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN_LOSE, None),
415-
'Rupees (Treasure Chest Game) (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE_LOSE, None),
416-
'Rupees (Treasure Chest Game) (20)': ('Item', None, GetItemId.GI_RUPEE_RED_LOSE, None),
417-
'Rupees (Treasure Chest Game) (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE_LOSE, None),
414+
'Rupee (Treasure Chest Game) (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN_LOSE, {'market_price': 1}),
415+
'Rupees (Treasure Chest Game) (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE_LOSE, {'market_price': 5}),
416+
'Rupees (Treasure Chest Game) (20)': ('Item', None, GetItemId.GI_RUPEE_RED_LOSE, {'market_price': 20}),
417+
'Rupees (Treasure Chest Game) (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE_LOSE, {'market_price': 50}),
418418
'Piece of Heart (Treasure Chest Game)': ('Item', True, GetItemId.GI_HEART_PIECE_WIN, {'alias': ('Piece of Heart', 1), 'progressive': float('Inf')}),
419419
'Ice Trap': ('Item', None, GetItemId.GI_ICE_TRAP, {'junk': 0}),
420420
'Progressive Hookshot': ('Item', True, GetItemId.GI_PROGRESSIVE_HOOKSHOT, {'progressive': 2}),
@@ -544,7 +544,7 @@ class GetItemId(IntEnum):
544544
'Ocarina C down Button': ('Item', True, GetItemId.GI_OCARINA_BUTTON_C_DOWN, {'ocarina_button': True}),
545545
'Ocarina C left Button': ('Item', True, GetItemId.GI_OCARINA_BUTTON_C_LEFT, {'ocarina_button': True}),
546546
'Ocarina C right Button': ('Item', True, GetItemId.GI_OCARINA_BUTTON_C_RIGHT, {'ocarina_button': True}),
547-
'Fairy Drop': ('Item', None, GetItemId.GI_FAIRY, None),
547+
'Fairy Drop': ('Item', None, GetItemId.GI_FAIRY, {'market_price': 50}),
548548
'Nothing': ('Item', None, GetItemId.GI_NOTHING, None),
549549

550550
# Event items otherwise generated by generic event logic

World.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -704,18 +704,21 @@ def random_shop_prices(self) -> None:
704704
for location in region.locations:
705705
if location.type == 'Shop':
706706
if location.name[-1:] in shop_item_indexes[:shop_item_count]:
707-
if self.settings.special_deal_price_distribution == 'vanilla':
708-
self.shop_prices[location.name] = ItemInfo.items[location.vanilla_item].price
709-
elif self.settings.special_deal_price_max < self.settings.special_deal_price_min:
710-
raise ValueError('Maximum special deal price is lower than minimum, perhaps you meant to swap them?')
711-
elif self.settings.special_deal_price_max == self.settings.special_deal_price_min:
712-
self.shop_prices[location.name] = self.settings.special_deal_price_min
713-
elif self.settings.special_deal_price_distribution == 'betavariate':
714-
self.shop_prices[location.name] = self.settings.special_deal_price_min + int(random.betavariate(1.5, 2) * (self.settings.special_deal_price_max - self.settings.special_deal_price_min) / 5) * 5
715-
elif self.settings.special_deal_price_distribution == 'uniform':
716-
self.shop_prices[location.name] = random.randrange(self.settings.special_deal_price_min, self.settings.special_deal_price_max + 1, 5)
717-
else:
718-
raise NotImplementedError(f'Unimplemented special deal distribution: {self.settings.special_deal_price_distribution}')
707+
self.shop_prices[location.name] = self.new_shop_price()
708+
709+
def new_shop_price(self) -> int:
710+
if self.settings.special_deal_price_distribution == 'vanilla':
711+
return ItemInfo.items[location.vanilla_item].price
712+
elif self.settings.special_deal_price_max < self.settings.special_deal_price_min:
713+
raise ValueError('Maximum special deal price is lower than minimum, perhaps you meant to swap them?')
714+
elif self.settings.special_deal_price_max == self.settings.special_deal_price_min:
715+
return self.settings.special_deal_price_min
716+
elif self.settings.special_deal_price_distribution == 'betavariate':
717+
return self.settings.special_deal_price_min + int(random.betavariate(1.5, 2) * (self.settings.special_deal_price_max - self.settings.special_deal_price_min) / 5) * 5
718+
elif self.settings.special_deal_price_distribution == 'uniform':
719+
return random.randrange(self.settings.special_deal_price_min, self.settings.special_deal_price_max + 1, 5)
720+
else:
721+
raise NotImplementedError(f'Unimplemented special deal distribution: {self.settings.special_deal_price_distribution}')
719722

720723
def set_scrub_prices(self) -> None:
721724
# Get Deku Scrub Locations
@@ -1216,10 +1219,20 @@ def push_item(self, location: str | Location, item: Item, manual: bool = False)
12161219
if not isinstance(location, Location):
12171220
location = self.get_location(location)
12181221

1222+
price: Optional[int]
1223+
if location.price is not None: # special deal
1224+
price = location.price
1225+
if item.info.market_price is not None and not (item.info.market_price_non_chu_drops_only and self.settings.free_bombchu_drops) and location.price >= item.info.market_price:
1226+
# Reduce the frequency of obvious scams by rerolling the price once if it's too high, and taking the lower value.
1227+
# This affects logic so it should only be applied to refills that are logically irrelevant.
1228+
# Otherwise there could be seeds with e.g. a wallet that's hinted as logically required for a purchase even though the price was rerolled to no longer require the wallet.
1229+
price = min(location.price, self.new_shop_price())
1230+
else:
1231+
price = item.price
1232+
12191233
location.item = item
12201234
item.location = location
1221-
item.price = location.price if location.price is not None else item.price
1222-
location.price = item.price
1235+
item.price = location.price = price
12231236

12241237
logging.getLogger('').debug('Placed %s [World %d] at %s [World %d]', item, item.world.id if hasattr(item, 'world') else -1, location, location.world.id if hasattr(location, 'world') else -1)
12251238

version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '9.0.14'
1+
__version__ = '9.0.15'
22

33
# This is a supplemental version number for branches based off of main dev.
44
supplementary_version = 0

0 commit comments

Comments
 (0)