forked from mirror/Archipelago
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
265 lines
12 KiB
Python
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
|
|
|