Files
dockipelago/worlds/Schedule_I/items.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

265 lines
12 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING:
from .world import Schedule1World
ITEM_NAME_TO_ID = {}
RAW_ITEM_CLASSIFICATIONS = {}
fillers = []
traps = []
# Mapping from JSON classification strings to ItemClassification flags
CLASSIFICATION_MAP = {
"USEFUL": ItemClassification.useful,
"PROGRESSION": ItemClassification.progression,
"FILLER": ItemClassification.filler,
"PROGRESSION_SKIP_BALANCING": ItemClassification.progression_skip_balancing,
"TRAP": ItemClassification.trap
}
def load_items_data(data):
"""Load item data from JSON and populate ITEM_NAME_TO_ID and RAW_ITEM_CLASSIFICATIONS."""
global ITEM_NAME_TO_ID, RAW_ITEM_CLASSIFICATIONS
ITEM_NAME_TO_ID = {item.name: item.modern_id for item in data.items.values()}
RAW_ITEM_CLASSIFICATIONS = {item.name: item.classification for item in data.items.values()}
# Each Item instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Item class and override the "game" field.
class Schedule1Item(Item):
game = "Schedule I"
# To do this, it must define a function called world.get_filler_item_name(), which we will define in world.py later.
# For now, let's make a function that returns the name of a random filler item here in items.py.
def get_random_filler_item_name(world: Schedule1World) -> str:
# For this purpose, we need to use a random generator.
# IMPORTANT: Whenever you need to use a random generator, you must use world.random.
# This ensures that generating with the same generator seed twice yields the same output.
# DO NOT use a bare random object from Python's built-in random module.
# Check if we should generate a trap item based on the trap_chance option.
if world.random.randint(0, 99) < world.options.trap_chance:
return world.random.choice(traps)
# Otherwise, return a random filler item.
return world.random.choice(fillers)
def check_option_enabled(world: Schedule1World, option_name: str) -> bool:
"""Check if an option is enabled based on option name string."""
option_map = {
"randomize_customers": world.options.randomize_customers,
"randomize_dealers": world.options.randomize_dealers,
"randomize_suppliers": world.options.randomize_suppliers,
"randomize_level_unlocks": world.options.randomize_level_unlocks,
"randomize_cartel_influence": world.options.randomize_cartel_influence,
"randomize_business_properties": world.options.randomize_business_properties,
"randomize_drug_making_properties": world.options.randomize_drug_making_properties,
}
return bool(option_map.get(option_name, False))
def check_option_condition(world: Schedule1World, condition_key: str) -> bool:
"""
Parse and evaluate a compound option condition string.
Supports:
- Simple: "randomize_level_unlocks" (option must be true)
- Negation: "!randomize_level_unlocks" (option must be false)
- Compound AND: "randomize_level_unlocks&!randomize_customers"
(first must be true AND second must be false)
Returns True if the condition is satisfied, False otherwise.
"""
parts = condition_key.split('&')
for part in parts:
part = part.strip()
if not part:
continue
if part.startswith('!'):
option_name = part[1:]
expected_value = False
else:
option_name = part
expected_value = True
actual_value = check_option_enabled(world, option_name)
if actual_value != expected_value:
return False
return True
def resolve_classification(world: Schedule1World, classification_data) -> ItemClassification:
"""
Resolve the classification from raw JSON data based on world options.
classification_data can be:
- A string: "PROGRESSION"
- A list: ["PROGRESSION", "USEFUL"]
- A dict with conditions: {"!randomize_customers": ["PROGRESSION", "USEFUL"], "default": ["USEFUL"]}
"""
# Determine which classification strings to use
if isinstance(classification_data, dict):
# Conditional classification - find matching condition
classification_strings = None
for condition_key, value in classification_data.items():
if condition_key == "default":
continue # Handle default last
if check_option_condition(world, condition_key):
classification_strings = value
break
# Fall back to default if no condition matched
if classification_strings is None:
classification_strings = classification_data.get("default", "FILLER")
else:
classification_strings = classification_data
# Convert to ItemClassification
if isinstance(classification_strings, list):
classification = CLASSIFICATION_MAP[classification_strings[0]]
for class_name in classification_strings[1:]:
classification |= CLASSIFICATION_MAP[class_name]
else:
classification = CLASSIFICATION_MAP[classification_strings]
return classification
def create_item_with_correct_classification(world: Schedule1World, name: str) -> Schedule1Item:
# Our world class must have a create_item() function that can create any of our items by name at any time.
# So, we make this helper function that creates the item by name with the correct classification.
# Note: This function's content could just be the contents of world.create_item in world.py directly,
# but it seemed nicer to have it in its own function over here in items.py.
classification = resolve_classification(world, RAW_ITEM_CLASSIFICATIONS[name])
return Schedule1Item(name, classification, ITEM_NAME_TO_ID[name], world.player)
# With those two helper functions defined, let's now get to actually creating and submitting our itempool.
def create_all_items(world: Schedule1World, data) -> None:
# Creating items should generally be done via the world's create_item method.
# First, we create a list containing all the items that always exist.
itempool: list[Item] = []
# Create bundles bundles
# Hard coding the bundles here based on options is more efficient than adding them through the json data
for _ in range(world.options.number_of_cash_bundles):
itempool += [world.create_item("Cash Bundle")]
for _ in range(world.options.number_of_xp_bundles):
itempool += [world.create_item("XP Bundle")]
if world.options.randomize_level_unlocks:
# If the randomize_level_unlocks option is enabled, create all items tagged as "Level Up Reward".
itempool += [world.create_item(item.name) for item in data.items.values()
if "Level Up Reward" in item.tags]
# Add cartel influence items based on options
if world.options.randomize_cartel_influence:
if not world.options.randomize_customers:
for _ in range(world.options.cartel_influence_items_per_region):
itempool += [world.create_item(item.name) for item in data.items.values()
if "Cartel Influence" in item.tags and "Westville" not in item.tags
and "Suburbia" not in item.tags]
# Suburbia is required for Finishing the Job
for _ in range(world.options.cartel_influence_items_per_region):
itempool += [world.create_item(item.name) for item in data.items.values()
if "Cartel Influence" in item.tags and "Suburbia" in item.tags]
# Westville starts at 500 less cartel influence. Will have 5 less cartel items as well to declutter
# Westville is required for Vibin the Cybin
for _ in range(world.options.cartel_influence_items_per_region - 5):
itempool += [world.create_item(item.name) for item in data.items.values()
if "Cartel Influence" in item.tags and "Westville" in item.tags]
if world.options.randomize_business_properties:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Business Property" in item.tags]
if world.options.randomize_drug_making_properties:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Drug Making Property" in item.tags]
if world.options.randomize_dealers:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Dealer" in item.tags and "Default" not in item.tags]
if world.options.randomize_customers:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Customer" in item.tags and "Default" not in item.tags]
if world.options.randomize_suppliers:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Supplier" in item.tags and "Default" not in item.tags]
if world.options.randomize_sewer_key:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Sewer" in item.tags]
# Removed these from checks
if world.options.randomize_customers:
starting_kyle_cooley = world.create_item("Kyle Cooley Unlocked")
world.push_precollected(starting_kyle_cooley)
starting_austin_steiner = world.create_item("Austin Steiner Unlocked")
world.push_precollected(starting_austin_steiner)
starting_kathy_henderson = world.create_item("Kathy Henderson Unlocked")
world.push_precollected(starting_kathy_henderson)
starting_jessi_waters = world.create_item("Jessi Waters Unlocked")
world.push_precollected(starting_jessi_waters)
starting_sam_thompson = world.create_item("Sam Thompson Unlocked")
world.push_precollected(starting_sam_thompson)
starting_mick_lubbin = world.create_item("Mick Lubbin Unlocked")
world.push_precollected(starting_mick_lubbin)
# Set up traps
for item in data.items.values():
resolved_classification = resolve_classification(world, item.classification)
if resolved_classification == ItemClassification.trap:
# Create list of traps
traps.append(item.name)
filler_conditions = {
"Bad Filler" : world.options.ban_bad_filler_items,
"Ban Progression Skip" : world.options.ban_progression_skip_items}
# set up fillers
for item in data.items.values():
resolved_classification = resolve_classification(world, item.classification)
if resolved_classification == ItemClassification.filler:
is_valid = True
for tag, should_ban in filler_conditions.items():
if should_ban and tag in item.tags:
is_valid = False
break
if is_valid:
fillers.append(item.name)
# The length of our itempool is easy to determine, since we have it as a list.
number_of_items = len(itempool)
# What we actually want is the number of *unfilled* locations. Luckily, there is a helper method for this:
number_of_unfilled_locations = len(world.multiworld.get_unfilled_locations(world.player))
# Now, we just subtract the number of items from the number of locations to get the number of empty item slots.
needed_number_of_filler_items = number_of_unfilled_locations - number_of_items
# Finally, we create that many filler items and add them to the itempool.
itempool += [world.create_filler() for _ in range(needed_number_of_filler_items)]
# This is how the generator actually knows about the existence of our items.
world.multiworld.itempool += itempool