Skip to content
Open
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
273 changes: 5 additions & 268 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def copy_dependencies(zip_path, file):
from worlds.LauncherComponents import Component, SuffixIdentifier, components, Type, icon_paths
import randomizer.ShuffleExits as ShuffleExits
from archipelago.FillSettings import fillsettings
from archipelago.Prices import PriceGenerator
from Utils import open_filename
import shutil
import zlib
Expand Down Expand Up @@ -687,273 +688,6 @@ def stage_assert_generate(cls, multiworld: MultiWorld):
raise FileNotFoundError("Invalid DK64 ROM file, please make sure your ROM is a vanilla DK64 file in big endian.")
check_version()

def _get_slot_data(self):
"""Get the slot data."""
return {
# "death_link": self.options.death_link.value,
}

def _get_shared_shop_vendors(self, Kongs, Types):
"""Identify vendor/level combinations that have shared shops."""
from randomizer.Lists.Location import SharedShopLocations

shared_shop_vendors = set()

if not self.options.enable_shared_shops.value:
if not hasattr(self.spoiler.settings, "selected_shared_shops"):
self.spoiler.settings.selected_shared_shops = set()
return shared_shop_vendors, set()

# Get or create the set of available shared shops
if hasattr(self.spoiler.settings, "selected_shared_shops") and self.spoiler.settings.selected_shared_shops:
available_shared_shops = self.spoiler.settings.selected_shared_shops
else:
all_shared_shops = list(SharedShopLocations)
self.random.shuffle(all_shared_shops)
available_shared_shops = set(all_shared_shops[:10])
self.spoiler.settings.selected_shared_shops = available_shared_shops

# Build set of vendor/level combinations
for location_id, location in self.spoiler.LocationList.items():
if location.type == Types.Shop and location.kong == Kongs.any:
if location_id in available_shared_shops:
shared_shop_vendors.add((location.level, location.vendor))

return shared_shop_vendors, available_shared_shops

def _categorize_shop_locations(self, shared_shop_vendors, available_shared_shops, Kongs, Types):
"""Categorize shops into included and excluded based on settings."""
shop_locations = []
excluded_shop_locations = []
shops_per_kong = {kong: 0 for kong in Kongs}

for location_id, location in self.spoiler.LocationList.items():
if location.type != Types.Shop:
continue

# Check if shop is excluded by smaller_shops setting
if hasattr(location, "smallerShopsInaccessible") and location.smallerShopsInaccessible and self.options.smaller_shops.value:
excluded_shop_locations.append(location_id)
continue

# Check if shared shop is excluded
if location.kong == Kongs.any:
if not self.options.enable_shared_shops.value or location_id not in available_shared_shops:
excluded_shop_locations.append(location_id)
continue

# Check if kong shop is blocked by a shared shop at same vendor/level
if location.kong != Kongs.any and self.options.enable_shared_shops.value:
if (location.level, location.vendor) in shared_shop_vendors:
excluded_shop_locations.append(location_id)
continue

# Shop is included
shop_locations.append(location_id)
if location.kong != Kongs.any:
shops_per_kong[location.kong] += 1

return shop_locations, excluded_shop_locations, shops_per_kong

def _calculate_kong_averages(self, shops_per_kong, max_coins, percentage, min_max_coins, Kongs):
"""Calculate average price per shop for each kong."""
avg_prices_per_kong = {}

for kong in Kongs:
if kong == Kongs.any or shops_per_kong[kong] == 0:
continue

# Calculate budget based on max coins available to this kong
kong_budget = max_coins[kong] * percentage

# Use higher safety margins: 80% for easy/medium, 95% for hard mode
safety_margin = 0.80 if percentage < 0.85 else 0.95
target = max(1, kong_budget * safety_margin)
avg_prices_per_kong[kong] = target / shops_per_kong[kong]

return avg_prices_per_kong

def _generate_individual_prices(self, shop_locations, avg_prices_per_kong, progressive_avg_price, progressive_stddev, shopprices, DK64RItems, Kongs):
"""Generate random individual prices for shops and progressive items."""
individual_prices = {}

# Generate shop prices
for location_id in shop_locations:
location = self.spoiler.LocationList[location_id]

if shopprices == 0:
individual_prices[location_id] = 0
continue

# Determine average and stddev for this shop
if location.kong == Kongs.any:
kong_avg = progressive_avg_price
kong_stddev = progressive_stddev
else:
kong_avg = avg_prices_per_kong.get(location.kong, progressive_avg_price)
kong_stddev = kong_avg * 0.3

price = round(self.random.normalvariate(kong_avg, kong_stddev))
price = max(1, min(price, int(kong_avg * 2)))
individual_prices[location_id] = price

progressive_moves = {
DK64RItems.ProgressiveSlam: 3,
DK64RItems.ProgressiveAmmoBelt: 2,
DK64RItems.ProgressiveInstrumentUpgrade: 3,
}

for item, count in progressive_moves.items():
individual_prices[item] = []
for _ in range(count):
if shopprices == 0:
individual_prices[item].append(0)
else:
price = round(self.random.normalvariate(progressive_avg_price, progressive_stddev))
price = max(1, min(price, int(progressive_avg_price * 2)))
individual_prices[item].append(price)

return individual_prices

def _convert_to_cumulative_prices(self, individual_prices, shop_locations, max_cumulative_per_kong, Kongs):
"""Convert individual prices to cumulative running totals per kong.
This mimics the logic from determineFinalPriceAssortment:
- Progressive items add to all kongs' totals, price stored is average of all totals
- Kong-specific shops add to that kong's total only
- Cumulative prices are capped at max_cumulative_per_kong to ensure accessibility
"""
price_assignment = []

# Build list of price assignments
for key, value in individual_prices.items():
if isinstance(value, list):
# Progressive move - add multiple entries
for price in value:
price_assignment.append({"is_prog": True, "cost": price, "item": key, "kong": Kongs.any})
elif key in shop_locations:
# Shop location
location = self.spoiler.LocationList[key]
price_assignment.append({"is_prog": False, "cost": value, "item": key, "kong": location.kong})

# Shuffle and calculate cumulative prices
self.random.shuffle(price_assignment)
total_cost = [0] * 5
cumulative_prices = {}

for assignment in price_assignment:
kong = assignment["kong"]
written_price = assignment["cost"]

if kong == Kongs.any:
# Progressive item - add to all kongs, price is average of current totals
current_kong_total = 0
for kong_index in range(5):
current_kong_total += total_cost[kong_index]
total_cost[kong_index] += written_price
# Cap at maximum affordable amount
total_cost[kong_index] = min(total_cost[kong_index], max_cumulative_per_kong[kong_index])
written_price = int(current_kong_total / 5)
else:
# Kong-specific shop - add to that kong's total
total_cost[kong] += written_price
# Cap at maximum affordable amount
total_cost[kong] = min(total_cost[kong], max_cumulative_per_kong[kong])
written_price = total_cost[kong]

# Store cumulative price
key = assignment["item"]
if assignment["is_prog"]:
if key not in cumulative_prices:
cumulative_prices[key] = []
cumulative_prices[key].append(written_price)
else:
cumulative_prices[key] = written_price

return cumulative_prices

def _generate_archipelago_prices(self):
"""Generate custom shop prices for Archipelago.
Generates individual prices for each shop and progressive item, then converts them
to cumulative prices (running totals) per kong. Excluded shops are set to 0 cost.
"""
from randomizer.Enums.Items import Items as DK64RItems
from randomizer.Lists.Item import ItemList as DK64RItemList
from randomizer.Enums.Kongs import Kongs

# Constants
MAX_COINS = {
Kongs.donkey: 179,
Kongs.diddy: 183,
Kongs.lanky: 190,
Kongs.tiny: 198,
Kongs.chunky: 224,
}

PRICE_PERCENTAGES = {
0: 0.0, # free
1: 0.35, # easy
2: 0.55, # medium
3: 0.85, # hard
}

# Get settings
shopprices = self.options.shop_prices.value
percentage = PRICE_PERCENTAGES[shopprices]
min_max_coins = min(MAX_COINS.values())
# Cap cumulative prices at actual max coins (rainbow coins are tracked separately as collectibles)
max_cumulative_per_kong = {kong: MAX_COINS[kong] for kong in MAX_COINS}

# Categorize shops
shared_shop_vendors, available_shared_shops = self._get_shared_shop_vendors(Kongs, Types)
shop_locations, excluded_shop_locations, shops_per_kong = self._categorize_shop_locations(shared_shop_vendors, available_shared_shops, Kongs, Types)

# Calculate pricing averages
avg_prices_per_kong = self._calculate_kong_averages(shops_per_kong, MAX_COINS, percentage, min_max_coins, Kongs)
# Progressive items use 25% of budget, divided among 8 total progressive items
progressive_avg_price = (min_max_coins * percentage * 0.25) / 8 if shopprices > 0 else 0
progressive_stddev = progressive_avg_price * 0.3

# Generate individual prices
individual_prices = self._generate_individual_prices(shop_locations, avg_prices_per_kong, progressive_avg_price, progressive_stddev, shopprices, DK64RItems, Kongs)

# Add 0 prices for non-shop items and excluded shops
for item_id in DK64RItemList.keys():
if item_id not in individual_prices:
individual_prices[item_id] = 0

for location_id in excluded_shop_locations:
individual_prices[location_id] = 0

# Store and finalize prices
self.spoiler.settings.original_prices = individual_prices.copy()

if shopprices > 0:
# Convert to cumulative prices
cumulative_prices = self._convert_to_cumulative_prices(individual_prices, shop_locations, max_cumulative_per_kong, Kongs)

# Add 0 prices for items not in shops
for item_id in DK64RItemList.keys():
if item_id not in cumulative_prices:
cumulative_prices[item_id] = 0

# Add 0 prices for all location IDs not already priced
for location_id in self.spoiler.LocationList.keys():
if location_id not in cumulative_prices:
cumulative_prices[location_id] = 0

for location_id in excluded_shop_locations:
cumulative_prices[location_id] = 0

self.spoiler.settings.prices = cumulative_prices
else:
# Free prices - ensure all locations exist with 0 cost
for location_id in self.spoiler.LocationList.keys():
if location_id not in individual_prices:
individual_prices[location_id] = 0
self.spoiler.settings.prices = individual_prices.copy()

def generate_early(self):
"""Generate the world."""
# Check host setting for minimal logic and force glitchless if disabled
Expand Down Expand Up @@ -1224,7 +958,10 @@ def generate_early(self):
else:
self.spoiler.settings.selected_shared_shops = set()

self._generate_archipelago_prices()
# Generate custom shop prices for Archipelago
price_generator = PriceGenerator(self.spoiler, self.options, self.random)
price_generator.generate_prices()

# Handle Loading Zones - this will handle LO and (someday?) LZR appropriately
if self.spoiler.settings.shuffle_loading_zones != ShuffleLoadingZones.none:
if self.spoiler.settings.level_randomization != LevelRandomization.loadingzone:
Expand Down
2 changes: 1 addition & 1 deletion archipelago.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"minimum_ap_version": "0.6.5",
"world_version": "1.4.44",
"world_version": "1.4.45",
"authors": ["2dos", "AlmostSeagull", "Ballaam", "Green Bean", "Killklli", "Lrauq", "PoryGone", "Umed"],
"version": 7,
"compatible_version": 7,
Expand Down
Loading