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
234 lines
13 KiB
Python
234 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, List
|
|
|
|
from flask_caching import logger
|
|
|
|
from BaseClasses import CollectionState
|
|
from worlds.generic.Rules import add_rule, set_rule
|
|
|
|
#from .options import ChosenString
|
|
#from .logics import story_routes
|
|
#from .locations import ev_location_bank
|
|
from .rezdata.misns import misn_table
|
|
from .rezdata.ships import ship_table #, ShipDict
|
|
#from .locations import loc_type_offset
|
|
from .apdata.offsets import offsets_table as loc_type_offset
|
|
from .logics import possible_regions
|
|
#from .items import ev_item_bank
|
|
|
|
if TYPE_CHECKING:
|
|
from .world import EVNWorld
|
|
|
|
|
|
# COMPLETION_LOCATIONS = {
|
|
# "Take Polaris Home;Rebel I22 LAST-354": 354,
|
|
# "Take Llyrell to Korell; Vellos31 LAST-417": 417,
|
|
# "Take Krane to Earth;Fed43 LAST-474": 474,
|
|
# "A Parting Gift;Fed26 (forced) LAST-596": 596,
|
|
# "Return to Heraan;Auroran 029 LAST-686": 686,
|
|
# "Destroy McGowan;Pirate 011 LAST-712": 712,
|
|
# "Return to Ar'Za Iusia;Polaris 46-887": 887,
|
|
# }
|
|
|
|
def _ship_id_rule(ship_id: int) -> str:
|
|
ship_offset = loc_type_offset["ship"]
|
|
#return ev_item_bank[ship_offset + ship_id]["name"]
|
|
# apparently can't import items bank when this is run... so we have to manually recreate the name.
|
|
# I don't like this, because it could change...
|
|
ship_data = ship_table[ship_id]
|
|
return ship_data["name"].strip() + ship_data["id"]
|
|
|
|
def _min_cargo_rule(min_weight: int) -> List[str]:
|
|
ship_offset = loc_type_offset["ship"]
|
|
core_list = []
|
|
for ship_id, ship_data in ship_table.items():
|
|
ship_cargo = ship_data["cargo"]
|
|
if int(ship_cargo) >= min_weight:
|
|
#core_list.append(ship_id)
|
|
core_list.append(ship_data["name"].strip() + ship_data["id"])
|
|
# ret_list = []
|
|
# for ship_id in core_list:
|
|
# ret_list.append(ev_item_bank[ship_offset + ship_id]["name"])
|
|
# return ret_list
|
|
return core_list
|
|
|
|
def _min_ship_str_rule(min_str: int) -> List[str]:
|
|
ship_offset = loc_type_offset["ship"]
|
|
core_list = []
|
|
for ship_id, ship_data in ship_table.items():
|
|
ship_stat = ship_data["strength"]
|
|
if int(ship_stat) >= min_str:
|
|
#logger.info(f"Ship str rule, adding {min_str}, {ship_stat}, {ship_data['id']}")
|
|
core_list.append(ship_data["name"].strip() + ship_data["id"])
|
|
return core_list
|
|
|
|
|
|
def set_all_rules(world: EVNWorld) -> None:
|
|
# In order for AP to generate an item layout that is actually possible for the player to complete,
|
|
# we need to define rules for our Entrances and Locations.
|
|
# Note: Regions do not have rules, the Entrances connecting them do!
|
|
# We'll do entrances first, then locations, and then finally we set our victory condition.
|
|
|
|
set_all_entrance_rules(world)
|
|
set_all_location_rules(world)
|
|
set_completion_condition(world)
|
|
|
|
|
|
def set_all_entrance_rules(world: EVNWorld) -> None:
|
|
#return
|
|
#test = 1
|
|
# # First, we need to actually grab our entrances. Luckily, there is a helper method for this.
|
|
# overworld_to_bottom_right_room = world.get_entrance("Overworld to Bottom Right Room")
|
|
# overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
|
|
# right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
|
|
|
|
# # An access rule is a function. We can define this function like any other function.
|
|
# # This function must accept exactly one parameter: A "CollectionState".
|
|
# # A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
|
|
# # which regions they've reached, etc.
|
|
# # In an access rule, we can ask whether the player has a collected a certain item.
|
|
# # We can do this via the state.has(...) function.
|
|
# # This function takes an item name, a player number, and an optional count parameter (more on that below)
|
|
# # Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
|
|
# # our function needs to be locally defined so that it has access to the player number from the outer scope.
|
|
# # In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
|
|
# def can_destroy_bush(state: CollectionState) -> bool:
|
|
# return state.has("Sword", world.player)
|
|
|
|
# # Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
|
|
# # One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
|
|
# set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
|
|
|
# # Because the function has to be defined locally, most worlds prefer the lambda syntax.
|
|
# set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
|
|
|
# # Conditions can depend on event items.
|
|
# set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
|
|
|
|
# # Some entrance rules may only apply if the player enabled certain options.
|
|
# # In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
|
|
# # Overworld to the Top Middle Room.
|
|
# if world.options.hammer:
|
|
# overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
|
|
# set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
|
|
|
|
chosen_route = world.get_chosen_string()
|
|
for from_id, to_regions in chosen_route["region_connections"].items():
|
|
from_region = possible_regions[from_id]
|
|
for to_id in to_regions:
|
|
to_region = possible_regions[to_id]
|
|
entrance_name = f"{from_region['name']} to {to_region['name']}"
|
|
region_entrance = world.get_entrance(entrance_name)
|
|
for rule_type, rule_value in to_region["entrance_rules"].items():
|
|
match rule_type:
|
|
case "ship":
|
|
temp_ship_id = _ship_id_rule(rule_value)
|
|
#logger.info(f"Ship rule for: {temp_ship_id}")
|
|
set_rule(region_entrance, lambda state: state.has(temp_ship_id, world.player))
|
|
case "min_cargo":
|
|
temp_list = _min_cargo_rule(rule_value)
|
|
set_rule(region_entrance, lambda state: state.has_any(temp_list, world.player))
|
|
case "min_ship_str": # Note: Ship str is arbitrary number designers added, but fits our purposes well enough here
|
|
temp_list = _min_ship_str_rule(rule_value)
|
|
set_rule(region_entrance, lambda state: state.has_any(temp_list, world.player))
|
|
#case "min_checks":
|
|
# apparently this doesn't update how we would expect. Advice was to not use it.
|
|
# set_rule(region_entrance, lambda state: len(state.locations_checked))
|
|
# I don't want to recreate a list of all possible checks, but I can't import the item library either due to cross ref imports
|
|
# set_rule(region_entrance, lambda state: state.has_from_list_unique(temp_list, world.player))
|
|
#case _:
|
|
# do nothing
|
|
|
|
|
|
def set_all_location_rules(world: EVNWorld) -> None:
|
|
# NOTE: I'll have to care for missions that can't be repeated. Ex: the first tutorial mission - if you say "no" to accepting it, you can't get it later.
|
|
|
|
# # Let's see if event names have to be unique...
|
|
# for loc_completion_name in COMPLETION_LOCATIONS.keys():
|
|
# #world.multiworld.get_location(loc_completion_name, world.player).place_locked_item(world.create_event("Victory"))
|
|
# world.multiworld.get_location(loc_completion_name, world.player).place_locked_item(world.create_item("Victory"))
|
|
|
|
# if loc_name in COMPLETION_LOCATIONS:
|
|
# evregion.add_event(loc_name, "Victory", location_type=EVNLocation, item_type=items.EVNItem)
|
|
|
|
|
|
# chosen_route = story_routes[world.options.chosen_string.value]
|
|
# world.multiworld.get_location(ev_location_bank[chosen_route["final_mission"]], world.player).place_locked_item(world.create_item("Victory"))
|
|
chosen_route = world.get_chosen_string()
|
|
misn_offset = loc_type_offset["misn"]
|
|
loc_name = world.location_id_to_name[chosen_route["final_mission"] + misn_offset] # Do we not have a helper function for this?
|
|
world.multiworld.get_location(loc_name, world.player).place_locked_item(world.create_item("Victory"))
|
|
|
|
# Location rules work no differently from Entrance rules.
|
|
# Most of our locations are chests that can simply be opened by walking up to them.
|
|
# Thus, their logical requirements are covered by the Entrance rules of the Entrances that were required to
|
|
# reach the region that the chest sits in.
|
|
# However, our two enemies work differently.
|
|
# Entering the room with the enemy is not enough, you also need to have enough combat items to be able to defeat it.
|
|
# So, we need to set requirements on the Locations themselves.
|
|
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
|
|
|
|
# Sometimes, you may want to have different rules depending on the player's chosen options.
|
|
# There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
|
|
# right_room_enemy = world.get_location("Right Room Enemy Drop")
|
|
|
|
# # DON'T DO THIS!!!!
|
|
# set_rule(
|
|
# right_room_enemy,
|
|
# lambda state: (
|
|
# state.has("Sword", world.player)
|
|
# and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
|
|
# ),
|
|
# )
|
|
# # DON'T DO THIS!!!!
|
|
|
|
# # Now, what's actually wrong with this? It works perfectly fine, right?
|
|
# # If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
|
|
# # The access rule we just wrote does this correctly, so what's the problem?
|
|
# # The problem is performance.
|
|
# # Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
|
|
# # However, access rules in particular are by far the hottest code path in Archipelago.
|
|
# # An access rule will potentially be called thousands or even millions of times over the course of one generation.
|
|
# # As a result, access rules are the one place where it's really worth putting in some effort to optimize.
|
|
# # What's the performance problem here?
|
|
# # Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
|
|
# # Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
|
|
# # Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
|
|
# # Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
|
|
# # and instead having two *different* set_rule calls depending on which case we're in.
|
|
|
|
# if world.options.hard_mode:
|
|
# # If you have multiple conditions, you can obviously chain them via "or" or "and".
|
|
# # However, there are also the nice helper functions "state.has_any" and "state.has_all".
|
|
# set_rule(
|
|
# right_room_enemy,
|
|
# lambda state: (
|
|
# state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
|
|
# ),
|
|
# )
|
|
# else:
|
|
# set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
|
|
|
|
# # Another way to chain multiple conditions is via the add_rule function.
|
|
# # This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
|
|
# # In our case, it's pretty useful because hard mode and easy mode have different requirements.
|
|
# final_boss = world.get_location("Final Boss Defeated")
|
|
|
|
# # For the "known" requirements, it's still better to chain them using a normal "and" condition.
|
|
# add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
|
|
|
|
# if world.options.hard_mode:
|
|
# # You can check for multiple copies of an item by using the optional count parameter of state.has().
|
|
# add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
|
|
|
|
|
|
def set_completion_condition(world: EVNWorld) -> None:
|
|
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
|
|
# You can just set a completion condition directly like any other condition, referencing items the player receives:
|
|
#world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Starbridge"), world.player)
|
|
|
|
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
|
|
# So lets undo what we just did, and instead set the completion condition to:
|
|
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|
|
#world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Victory"), world.player) |