mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-09 16:48:15 -07:00
Compare commits
44 Commits
NewSoupVi-
...
0.6.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45994e344e | ||
|
|
51d5e1afae | ||
|
|
577b958c4d | ||
|
|
ce38d8ced6 | ||
|
|
d65fcf286d | ||
|
|
5a6a0b37d6 | ||
|
|
4a0a65d604 | ||
|
|
d25abfc305 | ||
|
|
0905e3ce32 | ||
|
|
ac84b272c5 | ||
|
|
e8a63abfa4 | ||
|
|
3fa2745c37 | ||
|
|
775065715d | ||
|
|
4e608b13ae | ||
|
|
886cc68051 | ||
|
|
146a314d22 | ||
|
|
18cf1bce36 | ||
|
|
f7e3f4e589 | ||
|
|
9f9765b78d | ||
|
|
8ae1a7da32 | ||
|
|
08ea3fe225 | ||
|
|
b81be6b4fc | ||
|
|
f1aca0fc46 | ||
|
|
d88fe99780 | ||
|
|
360a1384f2 | ||
|
|
d089b00ad5 | ||
|
|
c05a2adc38 | ||
|
|
7631242621 | ||
|
|
df48c3e718 | ||
|
|
9a755e64b2 | ||
|
|
34d362a003 | ||
|
|
b75cce5d41 | ||
|
|
a07faca2d9 | ||
|
|
8a1a715dc4 | ||
|
|
60a192b1b6 | ||
|
|
3b721e0365 | ||
|
|
3e16c20fce | ||
|
|
ec2c39e82f | ||
|
|
23d319247f | ||
|
|
c2c488410f | ||
|
|
8ea49e76db | ||
|
|
d834ecec6a | ||
|
|
f3000a89d4 | ||
|
|
aa2774a5d5 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -24,10 +24,10 @@ env:
|
|||||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||||
# we check the sha256 and require manual intervention if it was updated.
|
# we check the sha256 and require manual intervention if it was updated.
|
||||||
APPIMAGE_FORK: 'PopTracker'
|
APPIMAGE_FORK: 'PopTracker'
|
||||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||||
|
|
||||||
permissions: # permissions required for attestation
|
permissions: # permissions required for attestation
|
||||||
id-token: 'write'
|
id-token: 'write'
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -12,10 +12,10 @@ env:
|
|||||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||||
# we check the sha256 and require manual intervention if it was updated.
|
# we check the sha256 and require manual intervention if it was updated.
|
||||||
APPIMAGE_FORK: 'PopTracker'
|
APPIMAGE_FORK: 'PopTracker'
|
||||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||||
|
|
||||||
permissions: # permissions required for attestation
|
permissions: # permissions required for attestation
|
||||||
id-token: 'write'
|
id-token: 'write'
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Launcher.py" />
|
||||||
<option name="PARAMETERS" value="\"Build APWorlds\"" />
|
<option name="PARAMETERS" value=""Build APWorlds"" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
<option name="MODULE_MODE" value="false" />
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ class CommonContext:
|
|||||||
hint_cost: int | None
|
hint_cost: int | None
|
||||||
"""Current Hint Cost per Hint from the server"""
|
"""Current Hint Cost per Hint from the server"""
|
||||||
hint_points: int | None
|
hint_points: int | None
|
||||||
"""Current avaliable Hint Points from the server"""
|
"""Current available Hint Points from the server"""
|
||||||
player_names: dict[int, str]
|
player_names: dict[int, str]
|
||||||
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
||||||
|
|
||||||
|
|||||||
@@ -347,7 +347,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
|||||||
elif isinstance(new_value, list):
|
elif isinstance(new_value, list):
|
||||||
cleaned_value.extend(new_value)
|
cleaned_value.extend(new_value)
|
||||||
elif isinstance(new_value, dict):
|
elif isinstance(new_value, dict):
|
||||||
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
|
counter_value = Counter(cleaned_value)
|
||||||
|
counter_value.update(new_value)
|
||||||
|
cleaned_value = dict(counter_value)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||||
f" received {type(new_value).__name__}.")
|
f" received {type(new_value).__name__}.")
|
||||||
@@ -361,7 +363,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
|||||||
for element in new_value:
|
for element in new_value:
|
||||||
cleaned_value.remove(element)
|
cleaned_value.remove(element)
|
||||||
elif isinstance(new_value, dict):
|
elif isinstance(new_value, dict):
|
||||||
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
|
counter_value = Counter(cleaned_value)
|
||||||
|
counter_value.subtract(new_value)
|
||||||
|
cleaned_value = dict(counter_value)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
||||||
f" received {type(new_value).__name__}.")
|
f" received {type(new_value).__name__}.")
|
||||||
|
|||||||
@@ -218,12 +218,17 @@ def launch(exe, in_terminal=False):
|
|||||||
|
|
||||||
def create_shortcut(button: Any, component: Component) -> None:
|
def create_shortcut(button: Any, component: Component) -> None:
|
||||||
from pyshortcuts import make_shortcut
|
from pyshortcuts import make_shortcut
|
||||||
|
env = os.environ
|
||||||
|
if "APPIMAGE" in env:
|
||||||
|
script = env["ARGV0"]
|
||||||
|
wkdir = None # defaults to ~ on Linux
|
||||||
|
else:
|
||||||
script = sys.argv[0]
|
script = sys.argv[0]
|
||||||
wkdir = Utils.local_path()
|
wkdir = Utils.local_path()
|
||||||
|
|
||||||
script = f"{script} \"{component.display_name}\""
|
script = f"{script} \"{component.display_name}\""
|
||||||
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||||
startmenu=False, terminal=False, working_dir=wkdir)
|
startmenu=False, terminal=False, working_dir=wkdir, noexe=Utils.is_frozen())
|
||||||
button.menu.dismiss()
|
button.menu.dismiss()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1545,6 +1545,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
|||||||
default = ()
|
default = ()
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
display_name = "Plando Items"
|
display_name = "Plando Items"
|
||||||
|
visibility = Visibility.template | Visibility.spoiler
|
||||||
|
|
||||||
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
||||||
self.value = list(deepcopy(value))
|
self.value = list(deepcopy(value))
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
if __name__ == "__main__":
|
||||||
|
import ModuleUpdate
|
||||||
|
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
|
||||||
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
||||||
ToggleButton, MarkupDropdown, ResizableTextField)
|
ToggleButton, MarkupDropdown, ResizableTextField)
|
||||||
from kivy.uix.behaviors.button import ButtonBehavior
|
from kivy.uix.behaviors.button import ButtonBehavior
|
||||||
@@ -330,6 +336,11 @@ class OptionsCreator(ThemedApp):
|
|||||||
box.range.slider.dropdown.open()
|
box.range.slider.dropdown.open()
|
||||||
|
|
||||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
||||||
|
if option.default in option.special_range_names:
|
||||||
|
# value can get mismatched in this case
|
||||||
|
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
|
||||||
|
option.range_end)
|
||||||
|
box.range.tag.text = str(int(box.range.slider.value))
|
||||||
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
@@ -365,7 +376,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
# for some reason this fixes an issue causing some to not open
|
# for some reason this fixes an issue causing some to not open
|
||||||
dropdown.open()
|
dropdown.open()
|
||||||
|
|
||||||
default_random = option.default == "random"
|
default_string = isinstance(option.default, str)
|
||||||
main_button = VisualChoice(option=option, name=name)
|
main_button = VisualChoice(option=option, name=name)
|
||||||
main_button.bind(on_release=open_dropdown)
|
main_button.bind(on_release=open_dropdown)
|
||||||
|
|
||||||
@@ -377,7 +388,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
for choice in option.name_lookup
|
for choice in option.name_lookup
|
||||||
]
|
]
|
||||||
dropdown = MDDropdownMenu(caller=main_button, items=items)
|
dropdown = MDDropdownMenu(caller=main_button, items=items)
|
||||||
self.options[name] = option.name_lookup[option.default] if not default_random else option.default
|
self.options[name] = option.name_lookup[option.default] if not default_string else option.default
|
||||||
return main_button
|
return main_button
|
||||||
|
|
||||||
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
|
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
|
||||||
@@ -560,8 +571,11 @@ class OptionsCreator(ThemedApp):
|
|||||||
groups[group].append((name, option))
|
groups[group].append((name, option))
|
||||||
|
|
||||||
for group, options in groups.items():
|
for group, options in groups.items():
|
||||||
|
options = [(name, option) for name, option in options
|
||||||
|
if name and option.visibility & Visibility.simple_ui]
|
||||||
if not options:
|
if not options:
|
||||||
continue # Game Options can be empty if every other option is in another group
|
continue # Game Options can be empty if every other option is in another group
|
||||||
|
# Can also have an option group of options that should not render on simple ui
|
||||||
group_item = MDExpansionPanel(size_hint_y=None)
|
group_item = MDExpansionPanel(size_hint_y=None)
|
||||||
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
|
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
|
||||||
TrailingPressedIconButton(icon="chevron-right",
|
TrailingPressedIconButton(icon="chevron-right",
|
||||||
@@ -583,7 +597,6 @@ class OptionsCreator(ThemedApp):
|
|||||||
group_box.layout.orientation = "vertical"
|
group_box.layout.orientation = "vertical"
|
||||||
group_box.layout.spacing = dp(3)
|
group_box.layout.spacing = dp(3)
|
||||||
for name, option in options:
|
for name, option in options:
|
||||||
if name and option is not Removed and option.visibility & Visibility.simple_ui:
|
|
||||||
group_content.add_widget(self.create_option(option, name, cls))
|
group_content.add_widget(self.create_option(option, name, cls))
|
||||||
expansion_box.layout.add_widget(group_item)
|
expansion_box.layout.add_widget(group_item)
|
||||||
self.option_layout.add_widget(expansion_box)
|
self.option_layout.add_widget(expansion_box)
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ class PlayerLocationsTotal(TypedDict):
|
|||||||
total_locations: int
|
total_locations: int
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerGame(TypedDict):
|
||||||
|
team: int
|
||||||
|
player: int
|
||||||
|
game: str
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route("/tracker/<suuid:tracker>")
|
@api_endpoints.route("/tracker/<suuid:tracker>")
|
||||||
@cache.memoize(timeout=60)
|
@cache.memoize(timeout=60)
|
||||||
def tracker_data(tracker: UUID) -> dict[str, Any]:
|
def tracker_data(tracker: UUID) -> dict[str, Any]:
|
||||||
@@ -80,7 +86,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
|
|||||||
"""Slot aliases of all players."""
|
"""Slot aliases of all players."""
|
||||||
for team, players in all_players.items():
|
for team, players in all_players.items():
|
||||||
for player in players:
|
for player in players:
|
||||||
player_aliases.append({"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
|
player_aliases.append(
|
||||||
|
{"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
|
||||||
|
|
||||||
player_items_received: list[PlayerItemsReceived] = []
|
player_items_received: list[PlayerItemsReceived] = []
|
||||||
"""Items received by each player."""
|
"""Items received by each player."""
|
||||||
@@ -94,7 +101,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
|
|||||||
for team, players in all_players.items():
|
for team, players in all_players.items():
|
||||||
for player in players:
|
for player in players:
|
||||||
player_checks_done.append(
|
player_checks_done.append(
|
||||||
{"team": team, "player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))})
|
{"team": team, "player": player,
|
||||||
|
"locations": sorted(tracker_data.get_player_checked_locations(team, player))})
|
||||||
|
|
||||||
total_checks_done: list[TeamTotalChecks] = [
|
total_checks_done: list[TeamTotalChecks] = [
|
||||||
{"team": team, "checks_done": checks_done}
|
{"team": team, "checks_done": checks_done}
|
||||||
@@ -144,7 +152,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
|
|||||||
"""The current client status for each player."""
|
"""The current client status for each player."""
|
||||||
for team, players in all_players.items():
|
for team, players in all_players.items():
|
||||||
for player in players:
|
for player in players:
|
||||||
player_status.append({"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
|
player_status.append(
|
||||||
|
{"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"aliases": player_aliases,
|
"aliases": player_aliases,
|
||||||
@@ -207,12 +216,20 @@ def static_tracker_data(tracker: UUID) -> dict[str, Any]:
|
|||||||
player_locations_total.append(
|
player_locations_total.append(
|
||||||
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
|
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
|
||||||
|
|
||||||
|
player_game: list[PlayerGame] = []
|
||||||
|
"""The played game per player slot."""
|
||||||
|
for team, players in all_players.items():
|
||||||
|
for player in players:
|
||||||
|
player_game.append({"team": team, "player": player, "game": tracker_data.get_player_game(player)})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"groups": groups,
|
"groups": groups,
|
||||||
"datapackage": tracker_data._multidata["datapackage"],
|
"datapackage": tracker_data._multidata["datapackage"],
|
||||||
"player_locations_total": player_locations_total,
|
"player_locations_total": player_locations_total,
|
||||||
|
"player_game": player_game,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# It should be exceedingly rare that slot data is needed, so it's separated out.
|
# It should be exceedingly rare that slot data is needed, so it's separated out.
|
||||||
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
|
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
|
||||||
@cache.memoize(timeout=300)
|
@cache.memoize(timeout=300)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ players to rely upon each other to complete their game.
|
|||||||
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
|
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
|
||||||
players to randomize any of the supported games, and send items between them. This allows players of different
|
players to randomize any of the supported games, and send items between them. This allows players of different
|
||||||
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
|
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
|
||||||
Here is a list of our [Supported Games](https://archipelago.gg/games).
|
Here is a list of our [Supported Games](/games).
|
||||||
|
|
||||||
## Can I generate a single-player game with Archipelago?
|
## Can I generate a single-player game with Archipelago?
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ play, open the Settings Page, pick your settings, and click Generate Game.
|
|||||||
|
|
||||||
## How do I get started?
|
## How do I get started?
|
||||||
|
|
||||||
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
|
We have a [Getting Started](/tutorial/Archipelago/setup/en) guide that will help you get the
|
||||||
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
|
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
|
||||||
including multiple games, and hosting multiworlds on the website for ease and convenience.
|
including multiple games, and hosting multiworlds on the website for ease and convenience.
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ their multiworld.
|
|||||||
|
|
||||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
|
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
|
||||||
in that game belonging to other players are sent out automatically. This allows other players to continue to play
|
in that game belonging to other players are sent out automatically. This allows other players to continue to play
|
||||||
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
|
uninterrupted. Here is a list of all of our [Server Commands](/tutorial/Archipelago/commands/en).
|
||||||
|
|
||||||
## What happens if an item is placed somewhere it is impossible to get?
|
## What happens if an item is placed somewhere it is impossible to get?
|
||||||
|
|
||||||
|
|||||||
@@ -959,7 +959,7 @@ if "Timespinner" in network_data_package["games"]:
|
|||||||
|
|
||||||
timespinner_location_ids = {
|
timespinner_location_ids = {
|
||||||
"Present": list(range(1337000, 1337085)),
|
"Present": list(range(1337000, 1337085)),
|
||||||
"Past": list(range(1337086, 1337175)),
|
"Past": list(range(1337086, 1337157)) + list(range(1337159, 1337175)),
|
||||||
"Ancient Pyramid": [
|
"Ancient Pyramid": [
|
||||||
1337236,
|
1337236,
|
||||||
1337246, 1337247, 1337248, 1337249]
|
1337246, 1337247, 1337248, 1337249]
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
tag: tag
|
tag: tag
|
||||||
MDLabel:
|
MDLabel:
|
||||||
id: tag
|
id: tag
|
||||||
text: str(this.option.default) if this.option.default != "random" else this.option.range_start
|
text: str(this.option.default) if not isinstance(this.option.default, str) else str(this.option.range_start)
|
||||||
MDSlider:
|
MDSlider:
|
||||||
id: slider
|
id: slider
|
||||||
min: this.option.range_start
|
min: this.option.range_start
|
||||||
max: this.option.range_end
|
max: this.option.range_end
|
||||||
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if this.option.default != "random" else this.option.range_start
|
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if not isinstance(this.option.default, str) else this.option.range_start
|
||||||
step: 1
|
step: 1
|
||||||
step_point_size: 0
|
step_point_size: 0
|
||||||
MDSliderHandle:
|
MDSliderHandle:
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
text: text
|
text: text
|
||||||
MDButtonText:
|
MDButtonText:
|
||||||
id: text
|
id: text
|
||||||
text: this.option.get_option_name(this.option.default if this.option.default != "random" else list(this.option.options.values())[0])
|
text: this.option.get_option_name(this.option.default if not isinstance(this.option.default, str) else list(this.option.options.values())[0])
|
||||||
theme_text_color: "Primary"
|
theme_text_color: "Primary"
|
||||||
|
|
||||||
<VisualNamedRange>:
|
<VisualNamedRange>:
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
text: text
|
text: text
|
||||||
MDButtonText:
|
MDButtonText:
|
||||||
id: text
|
id: text
|
||||||
text: this.option.special_range_names.get(list(this.option.special_range_names.values()).index(this.option.default)) if this.option.default in this.option.special_range_names else "Custom"
|
text: this.option.default.title() if this.option.default in this.option.special_range_names else "Custom"
|
||||||
|
|
||||||
<VisualFreeText>:
|
<VisualFreeText>:
|
||||||
multiline: False
|
multiline: False
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ These get automatically added to the `archipelago.json` of an .apworld if it is
|
|||||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
||||||
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||||
|
|
||||||
### "Build apworlds" Launcher Component
|
### "Build APWorlds" Launcher Component
|
||||||
|
|
||||||
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
|
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
|
||||||
and add `archipelago.json` manifest files to them.
|
and add `archipelago.json` manifest files to them.
|
||||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||||
|
|||||||
@@ -269,7 +269,8 @@ placed on them.
|
|||||||
|
|
||||||
### PriorityLocations
|
### PriorityLocations
|
||||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
||||||
the pool.
|
the pool. Progression items without a deprioritized flag will be used first when filling priority_locations. Progression items with
|
||||||
|
a deprioritized flag will be used next.
|
||||||
|
|
||||||
### ItemLinks
|
### ItemLinks
|
||||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||||
|
|||||||
@@ -385,6 +385,8 @@ Will provide a dict of static tracker data with the following keys:
|
|||||||
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
||||||
- The number of checks found vs. total checks available per player (`player_locations_total`)
|
- The number of checks found vs. total checks available per player (`player_locations_total`)
|
||||||
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
||||||
|
- The game each player is playing (`player_game`)
|
||||||
|
- Provided as a list of objects with `team`, `player`, and `game`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```json
|
```json
|
||||||
@@ -409,10 +411,10 @@ Example:
|
|||||||
],
|
],
|
||||||
"datapackage": {
|
"datapackage": {
|
||||||
"Archipelago": {
|
"Archipelago": {
|
||||||
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb",
|
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb"
|
||||||
},
|
},
|
||||||
"The Messenger": {
|
"The Messenger": {
|
||||||
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b",
|
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player_locations_total": [
|
"player_locations_total": [
|
||||||
@@ -427,6 +429,18 @@ Example:
|
|||||||
"total_locations": 20
|
"total_locations": 20
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"player_game": [
|
||||||
|
{
|
||||||
|
"team": 0,
|
||||||
|
"player": 1,
|
||||||
|
"game": "Archipelago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"team": 0,
|
||||||
|
"player": 2,
|
||||||
|
"game": "The Messenger"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
11
kvui.py
11
kvui.py
@@ -35,6 +35,17 @@ Config.set("input", "mouse", "mouse,disable_multitouch")
|
|||||||
Config.set("kivy", "exit_on_escape", "0")
|
Config.set("kivy", "exit_on_escape", "0")
|
||||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||||
|
|
||||||
|
# Workaround for Kivy issue #9226.
|
||||||
|
# caused by kivy by default using probesysfs,
|
||||||
|
# which assumes all multi touch deviecs are touch screens.
|
||||||
|
# workaround provided by Snu of the kivy commmunity c:
|
||||||
|
from kivy.utils import platform
|
||||||
|
if platform == "linux":
|
||||||
|
options = Config.options("input")
|
||||||
|
for option in options:
|
||||||
|
if Config.get("input", option) == "probesysfs":
|
||||||
|
Config.remove_option("input", option)
|
||||||
|
|
||||||
# Workaround for an issue where importing kivy.core.window before loading sounds
|
# Workaround for an issue where importing kivy.core.window before loading sounds
|
||||||
# will hang the whole application on Linux once the first sound is loaded.
|
# will hang the whole application on Linux once the first sound is loaded.
|
||||||
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
|
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
colorama>=0.4.6
|
colorama>=0.4.6
|
||||||
websockets>=13.0.1,<14
|
websockets>=13.0.1,<14
|
||||||
PyYAML>=6.0.2
|
PyYAML>=6.0.3
|
||||||
jellyfish>=1.1.3
|
jellyfish>=1.2.1
|
||||||
jinja2>=3.1.6
|
jinja2>=3.1.6
|
||||||
schema>=0.7.7
|
schema>=0.7.8
|
||||||
kivy>=2.3.1
|
kivy>=2.3.1
|
||||||
bsdiff4>=1.2.6
|
bsdiff4>=1.2.6
|
||||||
platformdirs>=4.3.6
|
platformdirs>=4.5.0
|
||||||
certifi>=2025.4.26
|
certifi>=2025.11.12
|
||||||
cython>=3.0.12
|
cython>=3.2.1
|
||||||
cymem>=2.0.11
|
cymem>=2.0.13
|
||||||
orjson>=3.10.15
|
orjson>=3.11.4
|
||||||
typing_extensions>=4.12.2
|
typing_extensions>=4.15.0
|
||||||
pyshortcuts>=1.9.1
|
pyshortcuts>=1.9.6
|
||||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||||
kivymd>=2.0.1.dev0
|
kivymd>=2.0.1.dev0
|
||||||
|
|||||||
@@ -44,19 +44,19 @@ class TestOptions(unittest.TestCase):
|
|||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
"name": "ItemLinkGroup",
|
"name": "ItemLinkGroup",
|
||||||
"item_pool": ["Hammer", "Bow"],
|
"item_pool": ["Hammer", "Sword"],
|
||||||
"link_replacement": False,
|
"link_replacement": False,
|
||||||
"replacement_item": None,
|
"replacement_item": None,
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
# we really need some sort of test world but generic doesn't have enough items for this
|
# we really need some sort of test world but generic doesn't have enough items for this
|
||||||
world = AutoWorldRegister.world_types["A Link to the Past"]
|
world = AutoWorldRegister.world_types["APQuest"]
|
||||||
plando_options = PlandoOptions.from_option_string("bosses")
|
plando_options = PlandoOptions.from_option_string("bosses")
|
||||||
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
|
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
|
||||||
for link in item_links:
|
for link in item_links:
|
||||||
link.verify(world, "tester", plando_options)
|
link.verify(world, "tester", plando_options)
|
||||||
self.assertIn("Hammer", link.value[0]["item_pool"])
|
self.assertIn("Hammer", link.value[0]["item_pool"])
|
||||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
self.assertIn("Sword", link.value[0]["item_pool"])
|
||||||
|
|
||||||
# TODO test that the group created using these options has the items
|
# TODO test that the group created using these options has the items
|
||||||
|
|
||||||
|
|||||||
@@ -37,3 +37,23 @@ class TestPlayerOptions(unittest.TestCase):
|
|||||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||||
self.assertEqual(len(new_weights["set_1"]), 2)
|
self.assertEqual(len(new_weights["set_1"]), 2)
|
||||||
self.assertIn("option_d", new_weights["set_1"])
|
self.assertIn("option_d", new_weights["set_1"])
|
||||||
|
|
||||||
|
def test_update_dict_supports_negatives_and_zeroes(self):
|
||||||
|
original_options = {
|
||||||
|
"dict_1": {"a": 1, "b": -1},
|
||||||
|
"dict_2": {"a": 1, "b": -1},
|
||||||
|
}
|
||||||
|
new_weights = Generate.update_weights(
|
||||||
|
original_options,
|
||||||
|
{
|
||||||
|
"+dict_1": {"a": -2, "b": 2},
|
||||||
|
"-dict_2": {"a": 1, "b": 2},
|
||||||
|
},
|
||||||
|
"Tested",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
self.assertEqual(new_weights["dict_1"]["a"], -1)
|
||||||
|
self.assertEqual(new_weights["dict_1"]["b"], 1)
|
||||||
|
self.assertEqual(new_weights["dict_2"]["a"], 0)
|
||||||
|
self.assertEqual(new_weights["dict_2"]["b"], -3)
|
||||||
|
self.assertIn("a", new_weights["dict_2"])
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ if __name__ == "__main__":
|
|||||||
empty_file = str(Path(tempdir) / "empty")
|
empty_file = str(Path(tempdir) / "empty")
|
||||||
open(empty_file, "w").close()
|
open(empty_file, "w").close()
|
||||||
sys.argv += ["--config_override", empty_file] # tests #5541
|
sys.argv += ["--config_override", empty_file] # tests #5541
|
||||||
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
|
multis = [["APQuest"], ["Temp World"], ["APQuest", "Temp World"]]
|
||||||
p1_games: list[str] = []
|
p1_games: list[str] = []
|
||||||
data_paths: list[Path | None] = []
|
data_paths: list[Path | None] = []
|
||||||
rooms: list[str] = []
|
rooms: list[str] = []
|
||||||
multidata: Path | None
|
multidata: Path | None
|
||||||
|
|
||||||
copy_world("VVVVVV", "Temp World")
|
copy_world("APQuest", "Temp World")
|
||||||
try:
|
try:
|
||||||
for n, games in enumerate(multis, 1):
|
for n, games in enumerate(multis, 1):
|
||||||
print(f"Generating [{n}] {', '.join(games)} offline")
|
print(f"Generating [{n}] {', '.join(games)} offline")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def copy(src: str, dst: str) -> None:
|
|||||||
src_cls = AutoWorldRegister.world_types[src]
|
src_cls = AutoWorldRegister.world_types[src]
|
||||||
src_folder = Path(src_cls.__file__).parent
|
src_folder = Path(src_cls.__file__).parent
|
||||||
worlds_folder = src_folder.parent
|
worlds_folder = src_folder.parent
|
||||||
if (not src_cls.__file__.endswith("__init__.py") or not src_folder.is_dir()
|
if (not src_cls.__file__.endswith(("__init__.py", "world.py")) or not src_folder.is_dir()
|
||||||
or not (worlds_folder / "generic").is_dir()):
|
or not (worlds_folder / "generic").is_dir()):
|
||||||
raise ValueError(f"Unsupported layout for copy_world from {src}")
|
raise ValueError(f"Unsupported layout for copy_world from {src}")
|
||||||
dst_folder = worlds_folder / dst_folder_name
|
dst_folder = worlds_folder / dst_folder_name
|
||||||
@@ -28,9 +28,12 @@ def copy(src: str, dst: str) -> None:
|
|||||||
raise ValueError(f"Destination {dst_folder} already exists")
|
raise ValueError(f"Destination {dst_folder} already exists")
|
||||||
shutil.copytree(src_folder, dst_folder)
|
shutil.copytree(src_folder, dst_folder)
|
||||||
_new_worlds[dst] = str(dst_folder)
|
_new_worlds[dst] = str(dst_folder)
|
||||||
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
|
|
||||||
|
for potential_world_class_file in ("__init__.py", "world.py"):
|
||||||
|
with open(dst_folder / potential_world_class_file, "r", encoding="utf-8-sig") as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
|
r_src = re.escape(src)
|
||||||
|
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + r_src + r'[\'"]', f'game = "{dst}"', contents)
|
||||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ description: Almost blank test yaml
|
|||||||
name: Player{NUMBER}
|
name: Player{NUMBER}
|
||||||
|
|
||||||
game:
|
game:
|
||||||
Timespinner: 1 # what else
|
APQuest: 1 # what else
|
||||||
requires:
|
requires:
|
||||||
version: 0.2.6
|
version: 0.2.6
|
||||||
Timespinner: {}
|
APQuest: {}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import unittest
|
|||||||
|
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from worlds import AutoWorldRegister
|
from worlds import AutoWorldRegister
|
||||||
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
|
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet, Visibility
|
||||||
|
|
||||||
|
|
||||||
class TestOptionPresets(unittest.TestCase):
|
class TestOptionPresets(unittest.TestCase):
|
||||||
@@ -19,6 +19,9 @@ class TestOptionPresets(unittest.TestCase):
|
|||||||
# pass in all plando options in case a preset wants to require certain plando options
|
# pass in all plando options in case a preset wants to require certain plando options
|
||||||
# for some reason
|
# for some reason
|
||||||
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
||||||
|
if not (Visibility.complex_ui in option.visibility or Visibility.simple_ui in option.visibility):
|
||||||
|
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' is not "
|
||||||
|
f"visible in any supported UI.")
|
||||||
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
||||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import abc
|
import abc
|
||||||
|
from bisect import bisect_right
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
from typing import (TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterable,
|
||||||
|
Optional, Sequence, Tuple, TypeGuard, TypeVar, Union)
|
||||||
|
|
||||||
|
|
||||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from SNIClient import SNIContext
|
from SNIClient import SNIContext
|
||||||
|
|
||||||
|
SNES_READ_CHUNK_SIZE = 2048
|
||||||
|
"""
|
||||||
|
note: SNI v0.0.101 currently has a bug where reads from
|
||||||
|
RetroArch >2048 bytes will only return the last ~2048 bytes read.
|
||||||
|
https://github.com/alttpo/sni/issues/51
|
||||||
|
"""
|
||||||
|
|
||||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
||||||
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
||||||
components.append(component)
|
components.append(component)
|
||||||
@@ -91,3 +103,119 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
|||||||
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
||||||
""" override this with code to handle packages from the server """
|
""" override this with code to handle packages from the server """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True, order=True)
|
||||||
|
class Read:
|
||||||
|
""" snes memory read - address and size in bytes """
|
||||||
|
address: int
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class _MemRead:
|
||||||
|
location: Read
|
||||||
|
data: bytes
|
||||||
|
|
||||||
|
|
||||||
|
_T_Enum = TypeVar("_T_Enum", bound=enum.Enum)
|
||||||
|
|
||||||
|
|
||||||
|
class SnesData(Generic[_T_Enum]):
|
||||||
|
_ranges: Sequence[_MemRead]
|
||||||
|
""" sorted by address """
|
||||||
|
|
||||||
|
def __init__(self, ranges: Sequence[tuple[Read, bytes]]) -> None:
|
||||||
|
self._ranges = [_MemRead(r, d) for r, d in ranges]
|
||||||
|
|
||||||
|
def get(self, read: _T_Enum) -> bytes:
|
||||||
|
assert isinstance(read.value, Read), read.value
|
||||||
|
address = read.value.address
|
||||||
|
index = bisect_right(self._ranges, address, key=lambda r: r.location.address) - 1
|
||||||
|
assert index >= 0, (self._ranges, read.value)
|
||||||
|
mem_read = self._ranges[index]
|
||||||
|
sub_index = address - mem_read.location.address
|
||||||
|
return mem_read.data[sub_index:sub_index + read.value.size]
|
||||||
|
|
||||||
|
|
||||||
|
class SnesReader(Generic[_T_Enum]):
|
||||||
|
"""
|
||||||
|
how to use:
|
||||||
|
```
|
||||||
|
from enum import Enum
|
||||||
|
from worlds.AutoSNIClient import Read, SNIClient, SnesReader
|
||||||
|
|
||||||
|
class MyGameMemory(Enum):
|
||||||
|
game_mode = Read(WRAM_START + 0x0998, 1)
|
||||||
|
send_queue = Read(SEND_QUEUE_START, 8 * 127)
|
||||||
|
...
|
||||||
|
|
||||||
|
snes_reader = SnesReader(MyGameMemory)
|
||||||
|
|
||||||
|
snes_data = await snes_reader.read(ctx)
|
||||||
|
if snes_data is None:
|
||||||
|
snes_logger.info("error reading from snes")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_mode = snes_data.get(MyGameMemory.game_mode)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
_ranges: Sequence[Read]
|
||||||
|
""" sorted by address """
|
||||||
|
|
||||||
|
def __init__(self, reads: type[_T_Enum]) -> None:
|
||||||
|
self._ranges = self._make_ranges(reads)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_ranges(reads: type[enum.Enum]) -> Sequence[Read]:
|
||||||
|
|
||||||
|
unprocessed_reads: list[Read] = []
|
||||||
|
for e in reads:
|
||||||
|
assert isinstance(e.value, Read), (reads.__name__, e, e.value)
|
||||||
|
unprocessed_reads.append(e.value)
|
||||||
|
unprocessed_reads.sort()
|
||||||
|
|
||||||
|
ranges: list[Read] = []
|
||||||
|
for read in unprocessed_reads:
|
||||||
|
# v end of the previous range
|
||||||
|
if len(ranges) == 0 or read.address - (ranges[-1].address + ranges[-1].size) > 255:
|
||||||
|
ranges.append(read)
|
||||||
|
else: # combine with previous range
|
||||||
|
chunk_address = ranges[-1].address
|
||||||
|
assert read.address >= chunk_address, "sort() didn't work? or something"
|
||||||
|
original_chunk_size = ranges[-1].size
|
||||||
|
new_size = max((read.address + read.size) - chunk_address,
|
||||||
|
original_chunk_size)
|
||||||
|
ranges[-1] = Read(chunk_address, new_size)
|
||||||
|
logging.debug(f"{len(ranges)=} {max(r.size for r in ranges)=}")
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
async def read(self, ctx: "SNIContext") -> SnesData[_T_Enum] | None:
|
||||||
|
"""
|
||||||
|
returns `None` if reading fails,
|
||||||
|
otherwise returns the data for the registered `Enum`
|
||||||
|
"""
|
||||||
|
from SNIClient import snes_read
|
||||||
|
|
||||||
|
reads: list[tuple[Read, bytes]] = []
|
||||||
|
for r in self._ranges:
|
||||||
|
if r.size < SNES_READ_CHUNK_SIZE: # most common
|
||||||
|
response = await snes_read(ctx, r.address, r.size)
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
reads.append((r, response))
|
||||||
|
else: # big read
|
||||||
|
# Problems were reported with big reads,
|
||||||
|
# so we chunk it into smaller pieces.
|
||||||
|
read_so_far = 0
|
||||||
|
collection: list[bytes] = []
|
||||||
|
while read_so_far < r.size:
|
||||||
|
remaining_size = r.size - read_so_far
|
||||||
|
chunk_size = min(SNES_READ_CHUNK_SIZE, remaining_size)
|
||||||
|
response = await snes_read(ctx, r.address + read_so_far, chunk_size)
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
collection.append(response)
|
||||||
|
read_so_far += chunk_size
|
||||||
|
reads.append((r, b"".join(collection)))
|
||||||
|
return SnesData(reads)
|
||||||
|
|||||||
@@ -318,5 +318,5 @@ if not is_frozen():
|
|||||||
open_folder(apworlds_folder)
|
open_folder(apworlds_folder)
|
||||||
|
|
||||||
|
|
||||||
components.append(Component('Build APWorlds', func=_build_apworlds, cli=True,
|
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
||||||
description="Build APWorlds from loose-file world folders."))
|
description="Build APWorlds from loose-file world folders."))
|
||||||
|
|||||||
@@ -263,7 +263,6 @@ def generate_itempool(world):
|
|||||||
('Frog', 'Get Frog'),
|
('Frog', 'Get Frog'),
|
||||||
('Missing Smith', 'Return Smith'),
|
('Missing Smith', 'Return Smith'),
|
||||||
('Floodgate', 'Open Floodgate'),
|
('Floodgate', 'Open Floodgate'),
|
||||||
('Agahnim 1', 'Beat Agahnim 1'),
|
|
||||||
('Flute Activation Spot', 'Activated Flute'),
|
('Flute Activation Spot', 'Activated Flute'),
|
||||||
('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
|
('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"game": "APQuest",
|
"game": "APQuest",
|
||||||
"minimum_ap_version": "0.6.4",
|
"minimum_ap_version": "0.6.4",
|
||||||
"world_version": "1.0.0",
|
"world_version": "1.0.1",
|
||||||
"authors": ["NewSoupVi"]
|
"authors": ["NewSoupVi"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from math import sqrt
|
from math import sqrt
|
||||||
from random import choice, random
|
from random import choice, random
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from kivy.core.window import Keyboard, Window
|
from kivy.core.window import Keyboard, Window
|
||||||
from kivy.graphics import Color, Triangle
|
from kivy.graphics import Color, Triangle
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import pkgutil
|
|||||||
from collections.abc import Buffer
|
from collections.abc import Buffer
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Literal, NamedTuple, cast
|
from typing import Literal, NamedTuple, Protocol, cast
|
||||||
|
|
||||||
from bokeh.protocol import Protocol
|
|
||||||
from kivy.uix.image import CoreImage
|
from kivy.uix.image import CoreImage
|
||||||
|
|
||||||
from CommonClient import logger
|
from CommonClient import logger
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ def make_data_directory(dir_name: str) -> Path:
|
|||||||
gitignore = specific_data_directory / ".gitignore"
|
gitignore = specific_data_directory / ".gitignore"
|
||||||
|
|
||||||
with open(gitignore, "w") as f:
|
with open(gitignore, "w") as f:
|
||||||
f.write("*\n")
|
f.write(
|
||||||
|
"""*
|
||||||
|
!.gitignore
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
return specific_data_directory
|
return specific_data_directory
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Benötigte Software
|
## Benötigte Software
|
||||||
|
|
||||||
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||||
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||||
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
|
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
|
||||||
|
|
||||||
@@ -11,16 +11,16 @@
|
|||||||
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
|
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
|
||||||
Dafür musst du oder jemand den du kennst ein Spiel generieren.
|
Dafür musst du oder jemand den du kennst ein Spiel generieren.
|
||||||
Dieser Schritt wird hier nicht erklärt, aber du kannst den
|
Dieser Schritt wird hier nicht erklärt, aber du kannst den
|
||||||
[Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game) lesen.
|
[Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game) lesen.
|
||||||
|
|
||||||
Du musst außerdem [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
|
Du musst außerdem [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
|
||||||
und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren.
|
und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren.
|
||||||
|
|
||||||
Von hier ist es einfach, dich mit deinem Slot zu verbinden.
|
Von hier ist es einfach, dich mit deinem Slot zu verbinden.
|
||||||
|
|
||||||
### Webhost-Raum
|
### Webhost-Raum
|
||||||
|
|
||||||
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](archipelago.gg))
|
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](https://archipelago.gg))
|
||||||
kannst du einfach auf deinen Namen in der Spielerliste klicken.
|
kannst du einfach auf deinen Namen in der Spielerliste klicken.
|
||||||
Dies öffnet den Archipelago Launcher, welcher dich dann fragt,
|
Dies öffnet den Archipelago Launcher, welcher dich dann fragt,
|
||||||
ob du den Text Client oder den APQuest Client öffnen willst.
|
ob du den Text Client oder den APQuest Client öffnen willst.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||||
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||||
if not bundled with your version of Archipelago
|
if not bundled with your version of Archipelago
|
||||||
|
|
||||||
@@ -10,16 +10,16 @@
|
|||||||
|
|
||||||
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
||||||
This will not be explained here,
|
This will not be explained here,
|
||||||
but you can check the [Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game).
|
but you can check the [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game).
|
||||||
|
|
||||||
You also need to have [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installed
|
You also need to have [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installed
|
||||||
and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago.
|
and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago.
|
||||||
|
|
||||||
From here, connecting to your APQuest slot is easy. There are two scenarios.
|
From here, connecting to your APQuest slot is easy. There are two scenarios.
|
||||||
|
|
||||||
### Webhost Room
|
### Webhost Room
|
||||||
|
|
||||||
If your room is hosted on a WebHost (e.g. [archipelago.gg](archipelago.gg)),
|
If your room is hosted on a WebHost (e.g. [archipelago.gg](https://archipelago.gg)),
|
||||||
you should be able to simply click on your name in the player list.
|
you should be able to simply click on your name in the player list.
|
||||||
This will open the Archipelago Launcher
|
This will open the Archipelago Launcher
|
||||||
and ask you whether you want to connect with the Text Client or the APQuest Client.
|
and ask you whether you want to connect with the Text Client or the APQuest Client.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from .events import Event, LocationClearedEvent, VictoryEvent
|
from .events import Event, LocationClearedEvent, VictoryEvent
|
||||||
from .gameboard import Gameboard
|
from .gameboard import Gameboard
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle corr
|
|||||||
## Where is the options page?
|
## Where is the options page?
|
||||||
|
|
||||||
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
||||||
|
|
||||||
|
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty.
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ Does not need to be added at the start of a seed, as it does not create any slot
|
|||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
### Windows / Linux
|
||||||
|
Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
||||||
|
|
||||||
|
### Web
|
||||||
|
Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser.
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
@@ -35,6 +39,14 @@ Info:
|
|||||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
||||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
||||||
- Click the various `?` buttons for information on controls/how to play
|
- Click the various `?` buttons for information on controls/how to play
|
||||||
|
|
||||||
|
## Admin Settings
|
||||||
|
|
||||||
|
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room.
|
||||||
|
|
||||||
|
- You can disable APSudoku for the entire room, preventing any hints from being granted.
|
||||||
|
- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve.
|
||||||
|
|
||||||
## DeathLink Support
|
## DeathLink Support
|
||||||
|
|
||||||
If `DeathLink` is enabled when you click `Connect`:
|
If `DeathLink` is enabled when you click `Connect`:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ The Mod can be installed and played by following these steps (see the [Mod Downl
|
|||||||
2. Launch the game, if "OFFLINE" is visible in the upper-right corner of the screen, the Mod is working
|
2. Launch the game, if "OFFLINE" is visible in the upper-right corner of the screen, the Mod is working
|
||||||
|
|
||||||
### Create a Config (.yaml) File
|
### Create a Config (.yaml) File
|
||||||
The purpose of a YAML file is described in the [Basic Multiworld Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game).
|
The purpose of a YAML file is described in the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en#generating-a-game).
|
||||||
|
|
||||||
The [Player Options page](/games/Choo-Choo%20Charles/player-options) allows to configure personal options and export a config YAML file.
|
The [Player Options page](/games/Choo-Choo%20Charles/player-options) allows to configure personal options and export a config YAML file.
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Follow these steps to host a remote multiplayer or a local single-player session
|
|||||||
1. Double-click the **cccharles.apworld** to automatically install the world randomization logic
|
1. Double-click the **cccharles.apworld** to automatically install the world randomization logic
|
||||||
2. Put the **CCCharles.yaml** to **Archipelago/Players/** with the YAML of each player to host
|
2. Put the **CCCharles.yaml** to **Archipelago/Players/** with the YAML of each player to host
|
||||||
3. Launch the Archipelago launcher and click "Generate" to configure a game with the YAMLs in **Archipelago/output/**
|
3. Launch the Archipelago launcher and click "Generate" to configure a game with the YAMLs in **Archipelago/output/**
|
||||||
4. For a multiplayer session, go to the [Archipelago HOST GAME page](https://archipelago.gg/uploads)
|
4. For a multiplayer session, go to the [Archipelago HOST GAME page](/uploads)
|
||||||
5. Click "Upload File" and select the generated **AP_\<seed\>.zip** in **Archipelago/output/**
|
5. Click "Upload File" and select the generated **AP_\<seed\>.zip** in **Archipelago/output/**
|
||||||
6. Send the generated room page to each player
|
6. Send the generated room page to each player
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Le Mod peut être installé et joué en suivant les étapes suivantes (voir la s
|
|||||||
2. Lancer le jeu, si "OFFLINE" est visible dans le coin en haut à droite de l'écran, le Mod est actif
|
2. Lancer le jeu, si "OFFLINE" est visible dans le coin en haut à droite de l'écran, le Mod est actif
|
||||||
|
|
||||||
### Créer un Fichier de Configuration (.yaml)
|
### Créer un Fichier de Configuration (.yaml)
|
||||||
L'objectif d'un fichier YAML est décrit dans le [Guide d'Installation Basique du Multiworld](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game) (en anglais).
|
L'objectif d'un fichier YAML est décrit dans le [Guide d'Installation Basique du Multiworld](/tutorial/Archipelago/setup/en#generating-a-game) (en anglais).
|
||||||
|
|
||||||
La [page d'Options Joueur](/games/Choo-Choo%20Charles/player-options) permet de configurer des options personnelles et exporter un fichier de configuration YAML.
|
La [page d'Options Joueur](/games/Choo-Choo%20Charles/player-options) permet de configurer des options personnelles et exporter un fichier de configuration YAML.
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Suivre ces étapes pour héberger une session multijoueur à distance ou locale
|
|||||||
1. Double-cliquer sur **cccharles.apworld** pour installer automatiquement la logique de randomisation du monde
|
1. Double-cliquer sur **cccharles.apworld** pour installer automatiquement la logique de randomisation du monde
|
||||||
2. Placer le **CCCharles.yaml** dans **Archipelago/Players/** avec le YAML de chaque joueur à héberger
|
2. Placer le **CCCharles.yaml** dans **Archipelago/Players/** avec le YAML de chaque joueur à héberger
|
||||||
3. Exécuter le lanceur Archipelago et cliquer sur "Generate" pour configurer une partie avec les YAML dans **Archipelago/output/**
|
3. Exécuter le lanceur Archipelago et cliquer sur "Generate" pour configurer une partie avec les YAML dans **Archipelago/output/**
|
||||||
4. Pour une session multijoueur, aller à la [page Archipelago HOST GAME](https://archipelago.gg/uploads)
|
4. Pour une session multijoueur, aller à la [page Archipelago HOST GAME](/uploads)
|
||||||
5. Cliquer sur "Upload File" et sélectionner le **AP_\<seed\>.zip** généré dans **Archipelago/output/**
|
5. Cliquer sur "Upload File" et sélectionner le **AP_\<seed\>.zip** généré dans **Archipelago/output/**
|
||||||
6. Envoyer la page de la partie générée à chaque joueur
|
6. Envoyer la page de la partie générée à chaque joueur
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ class CivVIContainer(APPlayerContainer):
|
|||||||
opened_zipfile.writestr(filename, yml)
|
opened_zipfile.writestr(filename, yml)
|
||||||
super().write_contents(opened_zipfile)
|
super().write_contents(opened_zipfile)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_value(value: str) -> str:
|
def sanitize_value(value: str) -> str:
|
||||||
"""Removes values that can cause issues in XML"""
|
"""Removes values that can cause issues in XML"""
|
||||||
return value.replace('"', "'").replace('&', 'and')
|
return value.replace('"', "'").replace('&', 'and').replace('{', '').replace('}', '')
|
||||||
|
|
||||||
|
|
||||||
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
||||||
@@ -87,8 +88,10 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
|||||||
boost_civics = []
|
boost_civics = []
|
||||||
|
|
||||||
if world.options.boostsanity:
|
if world.options.boostsanity:
|
||||||
boost_techs = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
|
boost_techs = [location for location in locations if location.location_type ==
|
||||||
boost_civics = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
|
CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
|
||||||
|
boost_civics = [location for location in locations if location.location_type ==
|
||||||
|
CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
|
||||||
techs += boost_techs
|
techs += boost_techs
|
||||||
civics += boost_civics
|
civics += boost_civics
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ renon_item_dialogue = {
|
|||||||
"\"Banshee Boomerang.\"",
|
"\"Banshee Boomerang.\"",
|
||||||
0x10: "No weapon triangle\n"
|
0x10: "No weapon triangle\n"
|
||||||
"advantages with this.",
|
"advantages with this.",
|
||||||
0x12: "It looks sus? Trust me,"
|
0x12: "It looks sus? Trust me,\n"
|
||||||
"my wares are genuine.",
|
"my wares are genuine.",
|
||||||
0x15: "This non-volatile kind\n"
|
0x15: "This non-volatile kind\n"
|
||||||
"is safe to handle.",
|
"is safe to handle.",
|
||||||
|
|||||||
@@ -247,6 +247,10 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, [0 for _ in range(12)], "EWRAM")])
|
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, [0 for _ in range(12)], "EWRAM")])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# If the player doesn't have Dash Boots for whatever reason, put them in their inventory now.
|
||||||
|
if not magic_items_array[0]:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(MAGIC_ITEMS_ARRAY_START, [1], "EWRAM")])
|
||||||
|
|
||||||
# Enable DeathLink if it's in our slot_data.
|
# Enable DeathLink if it's in our slot_data.
|
||||||
if "DeathLink" not in ctx.tags and ctx.slot_data["death_link"]:
|
if "DeathLink" not in ctx.tags and ctx.slot_data["death_link"]:
|
||||||
await ctx.update_death_link(True)
|
await ctx.update_death_link(True)
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ class DarkSouls3World(World):
|
|||||||
self.created_regions = set()
|
self.created_regions = set()
|
||||||
self.all_excluded_locations.update(self.options.exclude_locations.value)
|
self.all_excluded_locations.update(self.options.exclude_locations.value)
|
||||||
|
|
||||||
|
# This code doesn't work because tests don't verify options
|
||||||
|
# Don't consider disabled locations to be AP-excluded
|
||||||
|
# if not self.options.enable_dlc:
|
||||||
|
# self.options.exclude_locations.value = {
|
||||||
|
# location
|
||||||
|
# for location in self.options.exclude_locations
|
||||||
|
# if not location_dictionary[location].dlc
|
||||||
|
# }
|
||||||
|
|
||||||
|
# if not self.options.enable_ngp:
|
||||||
|
# self.options.exclude_locations.value = {
|
||||||
|
# location for
|
||||||
|
# location in self.options.exclude_locations
|
||||||
|
# if not location_dictionary[location].ngp
|
||||||
|
# }
|
||||||
|
|
||||||
# Inform Universal Tracker where Yhorm is being randomized to.
|
# Inform Universal Tracker where Yhorm is being randomized to.
|
||||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||||
if "Dark Souls III" in self.multiworld.re_gen_passthrough:
|
if "Dark Souls III" in self.multiworld.re_gen_passthrough:
|
||||||
@@ -264,6 +280,13 @@ class DarkSouls3World(World):
|
|||||||
):
|
):
|
||||||
new_location.progress_type = LocationProgressType.EXCLUDED
|
new_location.progress_type = LocationProgressType.EXCLUDED
|
||||||
else:
|
else:
|
||||||
|
# Don't consider non-randomized locations to be AP-excluded
|
||||||
|
if location.name in excluded:
|
||||||
|
excluded.remove(location.name)
|
||||||
|
# Only remove from all_excluded if excluded does not have priority over missable
|
||||||
|
if not (self.options.missable_location_behavior < self.options.excluded_location_behavior):
|
||||||
|
self.all_excluded_locations.remove(location.name)
|
||||||
|
|
||||||
# Don't allow missable duplicates of progression items to be expected progression.
|
# Don't allow missable duplicates of progression items to be expected progression.
|
||||||
if location.name in self.missable_dupe_prog_locs: continue
|
if location.name in self.missable_dupe_prog_locs: continue
|
||||||
|
|
||||||
@@ -283,11 +306,6 @@ class DarkSouls3World(World):
|
|||||||
parent = new_region,
|
parent = new_region,
|
||||||
)
|
)
|
||||||
new_location.place_locked_item(event_item)
|
new_location.place_locked_item(event_item)
|
||||||
if location.name in excluded:
|
|
||||||
excluded.remove(location.name)
|
|
||||||
# Only remove from all_excluded if excluded does not have priority over missable
|
|
||||||
if not (self.options.missable_location_behavior < self.options.excluded_location_behavior):
|
|
||||||
self.all_excluded_locations.remove(location.name)
|
|
||||||
|
|
||||||
new_region.locations.append(new_location)
|
new_region.locations.append(new_location)
|
||||||
|
|
||||||
@@ -1357,7 +1375,7 @@ class DarkSouls3World(World):
|
|||||||
if self.yhorm_location != default_yhorm_location:
|
if self.yhorm_location != default_yhorm_location:
|
||||||
text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n"
|
text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n"
|
||||||
|
|
||||||
if self.options.excluded_location_behavior == "allow_useful":
|
if self.options.excluded_location_behavior != "forbid_useful":
|
||||||
text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n"
|
text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n"
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ def set_rules(self) -> None:
|
|||||||
return True
|
return True
|
||||||
check_foresta(loc.parent_region)
|
check_foresta(loc.parent_region)
|
||||||
|
|
||||||
|
if self.options.map_shuffle or self.options.crest_shuffle:
|
||||||
|
process_rules(self.multiworld.get_entrance("Subregion Frozen Fields to Subregion Aquaria", self.player),
|
||||||
|
["SummerAquaria"])
|
||||||
|
|
||||||
if self.options.logic == "friendly":
|
if self.options.logic == "friendly":
|
||||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||||
["MagicMirror"])
|
["MagicMirror"])
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class GenericWeb(WebWorld):
|
|||||||
'A guide detailing the commands available to the user when participating in an Archipelago session.',
|
'A guide detailing the commands available to the user when participating in an Archipelago session.',
|
||||||
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
|
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
|
||||||
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
|
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
|
||||||
'English', 'mac_en.md','mac/en', ['Bicoloursnake'])
|
'English', 'mac_en.md','mac/en', ['Bicoloursnake', 'silasary'])
|
||||||
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
|
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
|
||||||
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
|
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
|
||||||
setup = Tutorial('Getting Started',
|
setup = Tutorial('Getting Started',
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ It is generally recommended that you use a virtual environment to run python bas
|
|||||||
4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom.
|
4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom.
|
||||||
5. Your client should now be running and rom created (where applicable).
|
5. Your client should now be running and rom created (where applicable).
|
||||||
## Additional Steps for SNES Games
|
## Additional Steps for SNES Games
|
||||||
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
||||||
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
||||||
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
|
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
|
||||||
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.
|
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
|
|||||||
"global_orbsanity_bundle_size": 10,
|
"global_orbsanity_bundle_size": 10,
|
||||||
"citizen_orb_trade_amount": 0,
|
"citizen_orb_trade_amount": 0,
|
||||||
"oracle_orb_trade_amount": 0,
|
"oracle_orb_trade_amount": 0,
|
||||||
"start_inventory": {"Power Cell": 100},
|
"fire_canyon_cell_count": 0,
|
||||||
|
"mountain_pass_cell_count": 0,
|
||||||
|
"lava_tube_cell_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_orb_items_are_filler(self):
|
def test_orb_items_are_filler(self):
|
||||||
@@ -26,7 +28,9 @@ class TradesCostEverythingTest(JakAndDaxterTestBase):
|
|||||||
"global_orbsanity_bundle_size": 10,
|
"global_orbsanity_bundle_size": 10,
|
||||||
"citizen_orb_trade_amount": 120,
|
"citizen_orb_trade_amount": 120,
|
||||||
"oracle_orb_trade_amount": 150,
|
"oracle_orb_trade_amount": 150,
|
||||||
"start_inventory": {"Power Cell": 100},
|
"fire_canyon_cell_count": 0,
|
||||||
|
"mountain_pass_cell_count": 0,
|
||||||
|
"lava_tube_cell_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_orb_items_are_progression(self):
|
def test_orb_items_are_progression(self):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ all_random = {
|
|||||||
"game_language": "random",
|
"game_language": "random",
|
||||||
"goal": "random",
|
"goal": "random",
|
||||||
"goal_speed": "random",
|
"goal_speed": "random",
|
||||||
"total_heart_stars": "random",
|
"max_heart_stars": "random",
|
||||||
"heart_stars_required": "random",
|
"heart_stars_required": "random",
|
||||||
"filler_percentage": "random",
|
"filler_percentage": "random",
|
||||||
"trap_percentage": "random",
|
"trap_percentage": "random",
|
||||||
@@ -34,7 +34,7 @@ all_random = {
|
|||||||
beginner = {
|
beginner = {
|
||||||
"goal": "zero",
|
"goal": "zero",
|
||||||
"goal_speed": "normal",
|
"goal_speed": "normal",
|
||||||
"total_heart_stars": 50,
|
"max_heart_stars": 50,
|
||||||
"heart_stars_required": 30,
|
"heart_stars_required": 30,
|
||||||
"filler_percentage": 25,
|
"filler_percentage": 25,
|
||||||
"trap_percentage": 0,
|
"trap_percentage": 0,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
|
|||||||
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
|
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
|
||||||
|
|
||||||
- Using a seed from the standalone KH2 Randomizer Seed Generator.
|
- Using a seed from the standalone KH2 Randomizer Seed Generator.
|
||||||
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](https://archipelago.gg/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
|
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import importlib.machinery
|
import importlib.machinery
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import pickle
|
import pickle
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
@@ -65,8 +63,27 @@ from .patches.aesthetics import rgb_to_bin, bin_to_rgb
|
|||||||
|
|
||||||
from .. import Options
|
from .. import Options
|
||||||
|
|
||||||
|
class VersionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
# Function to generate a final rom, this patches the rom with all required patches
|
# Function to generate a final rom, this patches the rom with all required patches
|
||||||
def generateRom(base_rom: bytes, args, patch_data: Dict):
|
def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||||
|
from .. import LinksAwakeningWorld
|
||||||
|
patcher_version = LinksAwakeningWorld.world_version
|
||||||
|
generated_version = Utils.tuplize_version(patch_data.get("generated_world_version", "2.0.0"))
|
||||||
|
if generated_version.major != patcher_version.major or generated_version.minor != patcher_version.minor:
|
||||||
|
Utils.messagebox(
|
||||||
|
"Error",
|
||||||
|
"The apworld version that this patch was generated on is incompatible with your installed world.\n\n"
|
||||||
|
f"Generated on {generated_version.as_simple_string()}\n"
|
||||||
|
f"Installed version {patcher_version.as_simple_string()}",
|
||||||
|
True
|
||||||
|
)
|
||||||
|
raise VersionError(
|
||||||
|
f"The installed world ({patcher_version.as_simple_string()}) is incompatible with the world this patch "
|
||||||
|
f"was generated on ({generated_version.as_simple_string()})"
|
||||||
|
)
|
||||||
|
|
||||||
random.seed(patch_data["seed"] + patch_data["player"])
|
random.seed(patch_data["seed"] + patch_data["player"])
|
||||||
multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
|
multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
|
||||||
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
|
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
|
||||||
@@ -85,9 +102,8 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
|||||||
pymod.prePatch(rom)
|
pymod.prePatch(rom)
|
||||||
|
|
||||||
if options["gfxmod"]:
|
if options["gfxmod"]:
|
||||||
user_settings = settings.get_settings()
|
|
||||||
try:
|
try:
|
||||||
gfx_mod_file = user_settings["ladx_options"]["gfx_mod_file"]
|
gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file
|
||||||
patches.aesthetics.gfxMod(rom, gfx_mod_file)
|
patches.aesthetics.gfxMod(rom, gfx_mod_file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass # if user just doesnt provide gfxmod file, let patching continue
|
pass # if user just doesnt provide gfxmod file, let patching continue
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ class BadRetroArchResponse(GameboyException):
|
|||||||
|
|
||||||
class BadRetroArchResponse(GameboyException):
|
class BadRetroArchResponse(GameboyException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VersionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LAClientConstants:
|
class LAClientConstants:
|
||||||
@@ -518,7 +522,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
class LADXManager(GameManager):
|
class LADXManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
("Tracker", "Tracker"),
|
("Tracker", "Tracker"),
|
||||||
]
|
]
|
||||||
base_title = f"Links Awakening DX Client {LinksAwakeningWorld.world_version.as_simple_string()} | Archipelago"
|
base_title = f"Links Awakening DX Client {LinksAwakeningWorld.world_version.as_simple_string()} | Archipelago"
|
||||||
|
|
||||||
@@ -614,11 +618,20 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
|
self.game = self.slot_info[self.slot].game
|
||||||
|
self.slot_data = args.get("slot_data", {})
|
||||||
|
generated_version = Utils.tuplize_version(self.slot_data.get("world_version", "2.0.0"))
|
||||||
|
client_version = LinksAwakeningWorld.world_version
|
||||||
|
if generated_version.major != client_version.major:
|
||||||
|
self.disconnected_intentionally = True
|
||||||
|
raise VersionError(
|
||||||
|
f"The installed world ({client_version.as_simple_string()}) is incompatible with "
|
||||||
f"the world this game was generated on ({generated_version.as_simple_string()})"
|
f"the world this game was generated on ({generated_version.as_simple_string()})"
|
||||||
)
|
)
|
||||||
# This is sent to magpie over local websocket to make its own connection
|
# This is sent to magpie over local websocket to make its own connection
|
||||||
self.slot_data.update({
|
self.slot_data.update({
|
||||||
"server_address": self.server_address,
|
"server_address": self.server_address,
|
||||||
|
"slot_name": self.player_names[self.slot],
|
||||||
"password": self.password,
|
"password": self.password,
|
||||||
"client_version": client_version.as_simple_string(),
|
"client_version": client_version.as_simple_string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import settings
|
|
||||||
import worlds.Files
|
import worlds.Files
|
||||||
import hashlib
|
import hashlib
|
||||||
import Utils
|
import Utils
|
||||||
@@ -59,6 +58,7 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
|
|||||||
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
|
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
|
||||||
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
|
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
|
||||||
data_dict = {
|
data_dict = {
|
||||||
|
"generated_world_version": world.world_version.as_simple_string(),
|
||||||
"out_base": world.multiworld.get_out_file_name_base(patch.player),
|
"out_base": world.multiworld.get_out_file_name_base(patch.player),
|
||||||
"is_race": world.multiworld.is_race,
|
"is_race": world.multiworld.is_race,
|
||||||
"seed": world.multiworld.seed,
|
"seed": world.multiworld.seed,
|
||||||
@@ -125,9 +125,9 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def get_base_rom_path(file_name: str = "") -> str:
|
def get_base_rom_path(file_name: str = "") -> str:
|
||||||
options = settings.get_settings()
|
from . import LinksAwakeningWorld
|
||||||
if not file_name:
|
if not file_name:
|
||||||
file_name = options["ladx_options"]["rom_file"]
|
file_name = LinksAwakeningWorld.settings.rom_file
|
||||||
if not os.path.exists(file_name):
|
if not os.path.exists(file_name):
|
||||||
file_name = Utils.user_path(file_name)
|
file_name = Utils.user_path(file_name)
|
||||||
return file_name
|
return file_name
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ class MagpieBridge:
|
|||||||
async def handler(self, websocket):
|
async def handler(self, websocket):
|
||||||
self.ws = websocket
|
self.ws = websocket
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
message = json.loads(await websocket.recv())
|
message = json.loads(await websocket.recv())
|
||||||
if message["type"] == "handshake":
|
if message["type"] == "handshake":
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -210,6 +211,8 @@ class MagpieBridge:
|
|||||||
await self.send_all_checks()
|
await self.send_all_checks()
|
||||||
if self.use_entrance_tracker():
|
if self.use_entrance_tracker():
|
||||||
await self.send_gps(diff=False)
|
await self.send_gps(diff=False)
|
||||||
|
except websockets.exceptions.ConnectionClosedOK:
|
||||||
|
pass
|
||||||
|
|
||||||
# Translate renamed IDs back to LADXR IDs
|
# Translate renamed IDs back to LADXR IDs
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import os
|
|||||||
import typing
|
import typing
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import struct
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
|
import Utils
|
||||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
|
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
|
||||||
from Fill import fill_restrictive
|
from Fill import fill_restrictive
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
@@ -50,6 +52,17 @@ class LinksAwakeningSettings(settings.Group):
|
|||||||
description = "LADX ROM File"
|
description = "LADX ROM File"
|
||||||
md5s = [LADXProcedurePatch.hash]
|
md5s = [LADXProcedurePatch.hash]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, path: str) -> None:
|
||||||
|
try:
|
||||||
|
super().validate(path)
|
||||||
|
except ValueError:
|
||||||
|
Utils.messagebox(
|
||||||
|
"Error",
|
||||||
|
"Provided rom does not match hash for English 1.0/revision-0 of Link's Awakening DX",
|
||||||
|
True)
|
||||||
|
raise
|
||||||
|
|
||||||
class RomStart(str):
|
class RomStart(str):
|
||||||
"""
|
"""
|
||||||
Set this to false to never autostart a rom (such as after patching)
|
Set this to false to never autostart a rom (such as after patching)
|
||||||
@@ -71,6 +84,24 @@ class LinksAwakeningSettings(settings.Group):
|
|||||||
Only .bin or .bdiff files
|
Only .bin or .bdiff files
|
||||||
The same directory will be checked for a matching text modification file
|
The same directory will be checked for a matching text modification file
|
||||||
"""
|
"""
|
||||||
|
def browse(self, filetypes=None, **kwargs):
|
||||||
|
filetypes = [("Binary / Patch files", [".bin", ".bdiff"])]
|
||||||
|
return super().browse(filetypes=filetypes, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, path: str) -> None:
|
||||||
|
with open(path, "rb", buffering=0) as f:
|
||||||
|
header, size = struct.unpack("<II", f.read()[:8])
|
||||||
|
if path.endswith('.bin') and header == 0xDEADBEEF and size < 1024:
|
||||||
|
# detect extended spritesheets from upstream ladxr
|
||||||
|
Utils.messagebox(
|
||||||
|
"Error",
|
||||||
|
"Extended sprite sheets are not supported. Try again with a different gfxmod file, "
|
||||||
|
"or provide no file to continue without modifying graphics.",
|
||||||
|
True)
|
||||||
|
raise ValueError("Provided gfxmod file is an extended sheet, which is not supported")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||||
rom_start: typing.Union[RomStart, bool] = True
|
rom_start: typing.Union[RomStart, bool] = True
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"game": "Links Awakening DX",
|
"game": "Links Awakening DX",
|
||||||
"authors": [ "zig", "threeandthree" ],
|
"authors": [ "zig", "threeandthree" ],
|
||||||
"minimum_ap_version": "0.6.4",
|
"minimum_ap_version": "0.6.4",
|
||||||
"world_version": "2.0.0"
|
"world_version": "2.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Additionally, if you get an item while already having the max for that item (for
|
|||||||
It is likely that you do not have release or collect permissions, or that there is nothing to release or collect.
|
It is likely that you do not have release or collect permissions, or that there is nothing to release or collect.
|
||||||
Another option is that your connection was interrupted.
|
Another option is that your connection was interrupted.
|
||||||
|
|
||||||
If you would still like to use release or collect, refer to [this section of the server commands page](https://archipelago.gg/tutorial/Archipelago/commands/en#collect/release).
|
If you would still like to use release or collect, refer to [this section of the server commands page](/tutorial/Archipelago/commands/en#collectrelease).
|
||||||
|
|
||||||
You may use the in-game console to execute the commands, if your slot has permissions to do so.
|
You may use the in-game console to execute the commands, if your slot has permissions to do so.
|
||||||
|
|
||||||
|
|||||||
@@ -374,22 +374,32 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi
|
|||||||
Handles `excluded_items`, `locked_items`, and `start_inventory`
|
Handles `excluded_items`, `locked_items`, and `start_inventory`
|
||||||
Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield.
|
Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield.
|
||||||
"""
|
"""
|
||||||
excluded_items = world.options.excluded_items
|
excluded_items: dict[str, int] = world.options.excluded_items.value
|
||||||
unexcluded_items = world.options.unexcluded_items
|
unexcluded_items: dict[str, int] = world.options.unexcluded_items.value
|
||||||
locked_items = world.options.locked_items
|
locked_items: dict[str, int] = world.options.locked_items.value
|
||||||
start_inventory = world.options.start_inventory
|
start_inventory: dict[str, int] = world.options.start_inventory.value
|
||||||
key_items = world.custom_mission_order.get_items_to_lock()
|
key_items = world.custom_mission_order.get_items_to_lock()
|
||||||
|
|
||||||
def resolve_count(count: Optional[int], max_count: int) -> int:
|
def resolve_exclude(count: int, max_count: int) -> int:
|
||||||
if count == 0:
|
if count < 0:
|
||||||
return max_count
|
return max_count
|
||||||
if count is None:
|
|
||||||
return 0
|
|
||||||
if max_count == 0:
|
|
||||||
return count
|
return count
|
||||||
return min(count, max_count)
|
|
||||||
|
|
||||||
auto_excludes = {item_name: 1 for item_name in item_groups.legacy_items}
|
def resolve_count(count: int, max_count: int, negative_value: int | None = None) -> int:
|
||||||
|
"""
|
||||||
|
Handles `count` being out of range.
|
||||||
|
* If `count > max_count`, returns `max_count`.
|
||||||
|
* If `count < 0`, returns `negative_value` (returns `max_count` if `negative_value` is unspecified)
|
||||||
|
"""
|
||||||
|
if count < 0:
|
||||||
|
if negative_value is None:
|
||||||
|
return max_count
|
||||||
|
return negative_value
|
||||||
|
if max_count and count > max_count:
|
||||||
|
return max_count
|
||||||
|
return count
|
||||||
|
|
||||||
|
auto_excludes = Counter({item_name: 1 for item_name in item_groups.legacy_items})
|
||||||
if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true:
|
if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true:
|
||||||
for item_name in item_groups.overpowered_items:
|
for item_name in item_groups.overpowered_items:
|
||||||
auto_excludes[item_name] = 1
|
auto_excludes[item_name] = 1
|
||||||
@@ -402,28 +412,29 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi
|
|||||||
elif item_name in item_groups.nova_equipment:
|
elif item_name in item_groups.nova_equipment:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
auto_excludes[item_name] = 0
|
auto_excludes[item_name] = item_data.quantity
|
||||||
|
|
||||||
|
|
||||||
result: List[FilterItem] = []
|
result: List[FilterItem] = []
|
||||||
for item_name, item_data in item_tables.item_table.items():
|
for item_name, item_data in item_tables.item_table.items():
|
||||||
max_count = item_data.quantity
|
max_count = item_data.quantity
|
||||||
auto_excluded_count = auto_excludes.get(item_name)
|
auto_excluded_count = auto_excludes.get(item_name, 0)
|
||||||
excluded_count = excluded_items.get(item_name, auto_excluded_count)
|
excluded_count = excluded_items.get(item_name, auto_excluded_count)
|
||||||
unexcluded_count = unexcluded_items.get(item_name)
|
unexcluded_count = unexcluded_items.get(item_name, 0)
|
||||||
locked_count = locked_items.get(item_name)
|
locked_count = locked_items.get(item_name, 0)
|
||||||
start_count: Optional[int] = start_inventory.get(item_name)
|
start_count = start_inventory.get(item_name, 0)
|
||||||
key_count = key_items.get(item_name, 0)
|
key_count = key_items.get(item_name, 0)
|
||||||
# specifying 0 in the yaml means exclude / lock all
|
# Specifying a negative number in the yaml means exclude / lock / start all.
|
||||||
# start_inventory doesn't allow specifying 0
|
# In the case of excluded/unexcluded, resolve negatives to max_count before subtracting them,
|
||||||
# not specifying means don't exclude/lock/start
|
# and after subtraction resolve negatives to just 0 (when unexcluded > excluded).
|
||||||
excluded_count = resolve_count(excluded_count, max_count)
|
excluded_count = resolve_count(
|
||||||
unexcluded_count = resolve_count(unexcluded_count, max_count)
|
resolve_exclude(excluded_count, max_count) - resolve_exclude(unexcluded_count, max_count),
|
||||||
|
max_count,
|
||||||
|
negative_value=0
|
||||||
|
)
|
||||||
locked_count = resolve_count(locked_count, max_count)
|
locked_count = resolve_count(locked_count, max_count)
|
||||||
start_count = resolve_count(start_count, max_count)
|
start_count = resolve_count(start_count, max_count)
|
||||||
|
|
||||||
excluded_count = max(0, excluded_count - unexcluded_count)
|
|
||||||
|
|
||||||
# Priority: start_inventory >> locked_items >> excluded_items >> unspecified
|
# Priority: start_inventory >> locked_items >> excluded_items >> unspecified
|
||||||
if max_count == 0:
|
if max_count == 0:
|
||||||
if excluded_count:
|
if excluded_count:
|
||||||
@@ -486,8 +497,9 @@ def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterIte
|
|||||||
item.flags |= ItemFilterFlags.FilterExcluded
|
item.flags |= ItemFilterFlags.FilterExcluded
|
||||||
continue
|
continue
|
||||||
if not zerg_missions and item.data.race == SC2Race.ZERG:
|
if not zerg_missions and item.data.race == SC2Race.ZERG:
|
||||||
if item.data.type != item_tables.ZergItemType.Ability \
|
if (item.data.type != item_tables.ZergItemType.Ability
|
||||||
and item.data.type != ZergItemType.Level:
|
and item.data.type != ZergItemType.Level
|
||||||
|
):
|
||||||
item.flags |= ItemFilterFlags.FilterExcluded
|
item.flags |= ItemFilterFlags.FilterExcluded
|
||||||
continue
|
continue
|
||||||
if not protoss_missions and item.data.race == SC2Race.PROTOSS:
|
if not protoss_missions and item.data.race == SC2Race.PROTOSS:
|
||||||
@@ -641,7 +653,7 @@ def flag_mission_based_item_excludes(world: SC2World, item_list: List[FilterItem
|
|||||||
item.flags |= ItemFilterFlags.FilterExcluded
|
item.flags |= ItemFilterFlags.FilterExcluded
|
||||||
|
|
||||||
# Remove Spear of Adun passives
|
# Remove Spear of Adun passives
|
||||||
if item.name in item_tables.spear_of_adun_castable_passives and not soa_passive_presence:
|
if item.name in item_groups.spear_of_adun_passives and not soa_passive_presence:
|
||||||
item.flags |= ItemFilterFlags.FilterExcluded
|
item.flags |= ItemFilterFlags.FilterExcluded
|
||||||
|
|
||||||
# Remove matchup-specific items if you don't play that matchup
|
# Remove matchup-specific items if you don't play that matchup
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from .options import (
|
|||||||
SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers,
|
SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers,
|
||||||
DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs,
|
DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs,
|
||||||
is_mission_in_soa_presence,
|
is_mission_in_soa_presence,
|
||||||
|
upgrade_included_names,
|
||||||
)
|
)
|
||||||
from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData
|
from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData
|
||||||
from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules
|
from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules
|
||||||
@@ -71,10 +72,12 @@ from .mission_tables import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
from .options import Option, upgrade_included_names
|
|
||||||
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
|
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from Options import Option
|
||||||
|
|
||||||
pool = concurrent.futures.ThreadPoolExecutor(1)
|
pool = concurrent.futures.ThreadPoolExecutor(1)
|
||||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||||
nest_asyncio.apply(loop)
|
nest_asyncio.apply(loop)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
This is usage documentation for the `custom_mission_order` YAML option for Starcraft 2. You can enable Custom Mission Orders by setting `mission_order: custom` in your YAML.
|
This is usage documentation for the `custom_mission_order` YAML option for Starcraft 2. You can enable Custom Mission Orders by setting `mission_order: custom` in your YAML.
|
||||||
|
|
||||||
You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) before continuing here.
|
You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](/tutorial/Archipelago/advanced_settings/en) before continuing here.
|
||||||
|
|
||||||
Every example in this document should be valid to generate.
|
Every example in this document should be valid to generate.
|
||||||
|
|
||||||
|
|||||||
@@ -775,7 +775,7 @@ item_descriptions = {
|
|||||||
item_names.BULLFROG_BROODLINGS: "Bullfrogs spawn two broodlings on impact, in addition to unloading their cargo.",
|
item_names.BULLFROG_BROODLINGS: "Bullfrogs spawn two broodlings on impact, in addition to unloading their cargo.",
|
||||||
item_names.BULLFROG_HARD_IMPACT: "Bullfrogs deal more damage and stun longer on impact.",
|
item_names.BULLFROG_HARD_IMPACT: "Bullfrogs deal more damage and stun longer on impact.",
|
||||||
item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: "Infested Banshees gain +100 life.",
|
item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: "Infested Banshees gain +100 life.",
|
||||||
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Infested Banshees regenerate 20 life and energy per second while burrowed.",
|
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Allows Infested Banshees to Burrow. Infested Banshees regenerate 20 life and energy per second while burrowed.",
|
||||||
item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: "Infested Banshees gain +2 range while cloaked.",
|
item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: "Infested Banshees gain +2 range while cloaked.",
|
||||||
item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: "Infested Liberators instantly transform into a cloud of microscopic organisms while attacking, reducing the damage they take by 85%.",
|
item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: "Infested Liberators instantly transform into a cloud of microscopic organisms while attacking, reducing the damage they take by 85%.",
|
||||||
item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: "Increases the damage Infested Liberators deal to their primary target by 100%.",
|
item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: "Increases the damage Infested Liberators deal to their primary target by 100%.",
|
||||||
@@ -951,14 +951,14 @@ item_descriptions = {
|
|||||||
item_names.TEMPEST_GRAVITY_SLING: "Tempests gain +8 range against air targets and +8 cast range.",
|
item_names.TEMPEST_GRAVITY_SLING: "Tempests gain +8 range against air targets and +8 cast range.",
|
||||||
item_names.TEMPEST_INTERPLANETARY_RANGE: "Tempests gain +8 weapon range against all targets.",
|
item_names.TEMPEST_INTERPLANETARY_RANGE: "Tempests gain +8 weapon range against all targets.",
|
||||||
item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: "Increases Phoenix, Mirage, and Skirmisher weapon damage by +2.",
|
item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: "Increases Phoenix, Mirage, and Skirmisher weapon damage by +2.",
|
||||||
item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmiser range by +2.",
|
item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmisher range by +2.",
|
||||||
item_names.CORSAIR_STEALTH_DRIVE: "Corsairs become permanently cloaked.",
|
item_names.CORSAIR_STEALTH_DRIVE: "Corsairs become permanently cloaked.",
|
||||||
item_names.CORSAIR_ARGUS_JEWEL: "Corsairs can store 2 charges of disruption web.",
|
item_names.CORSAIR_ARGUS_JEWEL: "Corsairs can store 2 charges of disruption web.",
|
||||||
item_names.CORSAIR_SUSTAINING_DISRUPTION: "Corsair disruption webs last longer.",
|
item_names.CORSAIR_SUSTAINING_DISRUPTION: "Corsair disruption webs last longer.",
|
||||||
item_names.CORSAIR_NEUTRON_SHIELDS: "Increases corsair maximum shields by +20.",
|
item_names.CORSAIR_NEUTRON_SHIELDS: "Increases corsair maximum shields by +20.",
|
||||||
item_names.ORACLE_STEALTH_DRIVE: "Oracles become permanently cloaked.",
|
item_names.ORACLE_STEALTH_DRIVE: "Oracles become permanently cloaked.",
|
||||||
item_names.ORACLE_SKYWARD_CHRONOANOMALY: "The Oracle's Stasis Ward can affect air units.",
|
item_names.ORACLE_SKYWARD_CHRONOANOMALY: "The Oracle's Stasis Ward can affect air units.",
|
||||||
item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to to spend energy to attack.",
|
item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to spend energy to attack.",
|
||||||
item_names.ORACLE_BOSONIC_CORE: "Increases starting energy by 150 and maximum energy by 50.",
|
item_names.ORACLE_BOSONIC_CORE: "Increases starting energy by 150 and maximum energy by 50.",
|
||||||
item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: "Arbiters gain +50 maximum life and +1 armor.",
|
item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: "Arbiters gain +50 maximum life and +1 armor.",
|
||||||
item_names.ARBITER_KHAYDARIN_CORE: _get_start_and_max_energy_desc("Arbiters"),
|
item_names.ARBITER_KHAYDARIN_CORE: _get_start_and_max_energy_desc("Arbiters"),
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ class ItemGroupNames:
|
|||||||
TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades"
|
TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades"
|
||||||
"""Progressive items where level 1 appeared in WoL"""
|
"""Progressive items where level 1 appeared in WoL"""
|
||||||
MENGSK_UNITS = "Mengsk Units"
|
MENGSK_UNITS = "Mengsk Units"
|
||||||
|
TERRAN_SC1_UNITS = "Terran SC1 Units"
|
||||||
|
TERRAN_SC1_BUILDINGS = "Terran SC1 Buildings"
|
||||||
|
TERRAN_LADDER_UNITS = "Terran Ladder Units"
|
||||||
TERRAN_VETERANCY_UNITS = "Terran Veterancy Units"
|
TERRAN_VETERANCY_UNITS = "Terran Veterancy Units"
|
||||||
ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities"
|
ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities"
|
||||||
WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities"
|
WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities"
|
||||||
@@ -154,6 +157,8 @@ class ItemGroupNames:
|
|||||||
"""All items from Stukov co-op subfaction"""
|
"""All items from Stukov co-op subfaction"""
|
||||||
INF_TERRAN_UNITS = "Infested Terran Units"
|
INF_TERRAN_UNITS = "Infested Terran Units"
|
||||||
INF_TERRAN_UPGRADES = "Infested Terran Upgrades"
|
INF_TERRAN_UPGRADES = "Infested Terran Upgrades"
|
||||||
|
ZERG_SC1_UNITS = "Zerg SC1 Units"
|
||||||
|
ZERG_LADDER_UNITS = "Zerg Ladder Units"
|
||||||
|
|
||||||
PROTOSS_ITEMS = "Protoss Items"
|
PROTOSS_ITEMS = "Protoss Items"
|
||||||
PROTOSS_UNITS = "Protoss Units"
|
PROTOSS_UNITS = "Protoss Units"
|
||||||
@@ -167,6 +172,7 @@ class ItemGroupNames:
|
|||||||
LOTV_UNITS = "LotV Units"
|
LOTV_UNITS = "LotV Units"
|
||||||
LOTV_ITEMS = "LotV Items"
|
LOTV_ITEMS = "LotV Items"
|
||||||
LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades"
|
LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades"
|
||||||
|
SOA_PASSIVES = "SOA Passive Abilities"
|
||||||
SOA_ITEMS = "SOA"
|
SOA_ITEMS = "SOA"
|
||||||
PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades"
|
PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades"
|
||||||
PROTOSS_BUILDINGS = "Protoss Buildings"
|
PROTOSS_BUILDINGS = "Protoss Buildings"
|
||||||
@@ -175,6 +181,9 @@ class ItemGroupNames:
|
|||||||
NERAZIM_UNITS = "Nerazim"
|
NERAZIM_UNITS = "Nerazim"
|
||||||
TAL_DARIM_UNITS = "Tal'Darim"
|
TAL_DARIM_UNITS = "Tal'Darim"
|
||||||
PURIFIER_UNITS = "Purifier"
|
PURIFIER_UNITS = "Purifier"
|
||||||
|
PROTOSS_SC1_UNITS = "Protoss SC1 Units"
|
||||||
|
PROTOSS_SC1_BUILDINGS = "Protoss SC1 Buildings"
|
||||||
|
PROTOSS_LADDER_UNITS = "Protoss Ladder Units"
|
||||||
|
|
||||||
VANILLA_ITEMS = "Vanilla Items"
|
VANILLA_ITEMS = "Vanilla Items"
|
||||||
OVERPOWERED_ITEMS = "Overpowered Items"
|
OVERPOWERED_ITEMS = "Overpowered Items"
|
||||||
@@ -287,8 +296,14 @@ item_name_groups[ItemGroupNames.WOL_BUILDINGS] = wol_buildings = [
|
|||||||
item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER,
|
item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER,
|
||||||
]
|
]
|
||||||
item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [
|
item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [
|
||||||
|
*[
|
||||||
item_name for item_name, item_data in item_tables.item_table.items()
|
item_name for item_name, item_data in item_tables.item_table.items()
|
||||||
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
|
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
|
||||||
|
],
|
||||||
|
item_names.PSI_SCREEN,
|
||||||
|
item_names.SONIC_DISRUPTER,
|
||||||
|
item_names.PSI_INDOCTRINATOR,
|
||||||
|
item_names.ARGUS_AMPLIFIER,
|
||||||
]
|
]
|
||||||
item_name_groups[ItemGroupNames.MENGSK_UNITS] = [
|
item_name_groups[ItemGroupNames.MENGSK_UNITS] = [
|
||||||
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW,
|
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW,
|
||||||
@@ -316,6 +331,41 @@ spider_mine_sources = [
|
|||||||
item_names.SIEGE_TANK_SPIDER_MINES,
|
item_names.SIEGE_TANK_SPIDER_MINES,
|
||||||
item_names.RAVEN_SPIDER_MINES,
|
item_names.RAVEN_SPIDER_MINES,
|
||||||
]
|
]
|
||||||
|
item_name_groups[ItemGroupNames.TERRAN_SC1_UNITS] = [
|
||||||
|
item_names.MARINE,
|
||||||
|
item_names.FIREBAT,
|
||||||
|
item_names.GHOST,
|
||||||
|
item_names.MEDIC,
|
||||||
|
item_names.VULTURE,
|
||||||
|
item_names.SIEGE_TANK,
|
||||||
|
item_names.GOLIATH,
|
||||||
|
item_names.WRAITH,
|
||||||
|
# No dropship
|
||||||
|
item_names.SCIENCE_VESSEL,
|
||||||
|
item_names.BATTLECRUISER,
|
||||||
|
item_names.VALKYRIE,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.TERRAN_SC1_BUILDINGS] = [
|
||||||
|
item_names.BUNKER,
|
||||||
|
item_names.MISSILE_TURRET,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.TERRAN_LADDER_UNITS] = [
|
||||||
|
item_names.MARINE,
|
||||||
|
item_names.MARAUDER,
|
||||||
|
item_names.REAPER,
|
||||||
|
item_names.GHOST,
|
||||||
|
item_names.HELLION,
|
||||||
|
item_names.WIDOW_MINE,
|
||||||
|
item_names.SIEGE_TANK,
|
||||||
|
item_names.THOR,
|
||||||
|
item_names.CYCLONE,
|
||||||
|
item_names.VIKING,
|
||||||
|
item_names.MEDIVAC,
|
||||||
|
item_names.LIBERATOR,
|
||||||
|
item_names.RAVEN,
|
||||||
|
item_names.BANSHEE,
|
||||||
|
item_names.BATTLECRUISER,
|
||||||
|
]
|
||||||
|
|
||||||
# Terran Upgrades
|
# Terran Upgrades
|
||||||
item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [
|
item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [
|
||||||
@@ -596,6 +646,38 @@ item_name_groups[ItemGroupNames.OVERLORD_UPGRADES] = [
|
|||||||
item_names.OVERLORD_IMPROVED_OVERLORDS,
|
item_names.OVERLORD_IMPROVED_OVERLORDS,
|
||||||
item_names.OVERLORD_OVERSEER_ASPECT,
|
item_names.OVERLORD_OVERSEER_ASPECT,
|
||||||
]
|
]
|
||||||
|
item_name_groups[ItemGroupNames.ZERG_SC1_UNITS] = [
|
||||||
|
item_names.ZERGLING,
|
||||||
|
item_names.HYDRALISK,
|
||||||
|
item_names.MUTALISK,
|
||||||
|
item_names.SCOURGE,
|
||||||
|
item_names.BROOD_QUEEN,
|
||||||
|
item_names.DEFILER,
|
||||||
|
item_names.ULTRALISK,
|
||||||
|
item_names.HYDRALISK_LURKER_ASPECT,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT,
|
||||||
|
item_names.DEVOURING_ONES,
|
||||||
|
item_names.HUNTER_KILLERS,
|
||||||
|
item_names.TORRASQUE_MERC,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.ZERG_LADDER_UNITS] = [
|
||||||
|
item_names.ZERGLING,
|
||||||
|
item_names.SWARM_QUEEN, # Replace: Hive Queen
|
||||||
|
item_names.ZERGLING_BANELING_ASPECT,
|
||||||
|
item_names.ROACH,
|
||||||
|
item_names.ROACH_RAVAGER_ASPECT,
|
||||||
|
item_names.OVERLORD_OVERSEER_ASPECT,
|
||||||
|
item_names.HYDRALISK,
|
||||||
|
item_names.HYDRALISK_LURKER_ASPECT,
|
||||||
|
item_names.MUTALISK,
|
||||||
|
item_names.CORRUPTOR,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT,
|
||||||
|
item_names.INFESTOR,
|
||||||
|
item_names.SWARM_HOST,
|
||||||
|
item_names.ULTRALISK,
|
||||||
|
]
|
||||||
|
|
||||||
# Zerg Upgrades
|
# Zerg Upgrades
|
||||||
item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [
|
item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [
|
||||||
@@ -777,11 +859,21 @@ item_name_groups[ItemGroupNames.PURIFIER_UNITS] = [
|
|||||||
item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST,
|
item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST,
|
||||||
item_names.CALADRIUS,
|
item_names.CALADRIUS,
|
||||||
]
|
]
|
||||||
item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = [
|
item_name_groups[ItemGroupNames.SOA_PASSIVES] = spear_of_adun_passives = [
|
||||||
|
item_names.RECONSTRUCTION_BEAM,
|
||||||
|
item_names.OVERWATCH,
|
||||||
|
item_names.GUARDIAN_SHELL,
|
||||||
|
]
|
||||||
|
spear_of_adun_actives = [
|
||||||
*[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun],
|
*[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun],
|
||||||
item_names.SOA_PROGRESSIVE_PROXY_PYLON,
|
item_names.SOA_PROGRESSIVE_PROXY_PYLON,
|
||||||
]
|
]
|
||||||
lotv_soa_items = [item_name for item_name in soa_items if item_name != item_names.SOA_PYLON_OVERCHARGE]
|
item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = spear_of_adun_actives + spear_of_adun_passives
|
||||||
|
lotv_soa_items = [
|
||||||
|
item_name
|
||||||
|
for item_name in soa_items
|
||||||
|
if item_name not in (item_names.SOA_PYLON_OVERCHARGE, item_names.OVERWATCH)
|
||||||
|
]
|
||||||
item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [
|
item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [
|
||||||
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core
|
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core
|
||||||
]
|
]
|
||||||
@@ -815,6 +907,45 @@ item_name_groups[ItemGroupNames.LOTV_ITEMS] = vanilla_lotv_items = (
|
|||||||
+ protoss_generic_upgrades
|
+ protoss_generic_upgrades
|
||||||
+ lotv_war_council_upgrades
|
+ lotv_war_council_upgrades
|
||||||
)
|
)
|
||||||
|
item_name_groups[ItemGroupNames.PROTOSS_SC1_UNITS] = [
|
||||||
|
item_names.ZEALOT,
|
||||||
|
item_names.DRAGOON,
|
||||||
|
item_names.HIGH_TEMPLAR,
|
||||||
|
item_names.DARK_TEMPLAR,
|
||||||
|
item_names.DARK_ARCHON,
|
||||||
|
item_names.DARK_TEMPLAR_DARK_ARCHON_MELD,
|
||||||
|
# No shuttle
|
||||||
|
item_names.REAVER,
|
||||||
|
item_names.OBSERVER,
|
||||||
|
item_names.SCOUT,
|
||||||
|
item_names.CARRIER,
|
||||||
|
item_names.ARBITER,
|
||||||
|
item_names.CORSAIR,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.PROTOSS_SC1_BUILDINGS] = [
|
||||||
|
item_names.PHOTON_CANNON,
|
||||||
|
item_names.SHIELD_BATTERY,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.PROTOSS_LADDER_UNITS] = [
|
||||||
|
item_names.ZEALOT,
|
||||||
|
item_names.STALKER,
|
||||||
|
item_names.SENTRY,
|
||||||
|
item_names.ADEPT,
|
||||||
|
item_names.HIGH_TEMPLAR,
|
||||||
|
item_names.DARK_TEMPLAR,
|
||||||
|
item_names.DARK_TEMPLAR_ARCHON_MERGE,
|
||||||
|
item_names.OBSERVER,
|
||||||
|
item_names.WARP_PRISM,
|
||||||
|
item_names.IMMORTAL,
|
||||||
|
item_names.COLOSSUS,
|
||||||
|
item_names.DISRUPTOR,
|
||||||
|
item_names.PHOENIX,
|
||||||
|
item_names.VOID_RAY,
|
||||||
|
item_names.ORACLE,
|
||||||
|
item_names.CARRIER,
|
||||||
|
item_names.TEMPEST,
|
||||||
|
item_names.MOTHERSHIP, # Replace: Aiur Mothership
|
||||||
|
]
|
||||||
|
|
||||||
item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = (
|
item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = (
|
||||||
vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items
|
vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items
|
||||||
|
|||||||
@@ -2151,127 +2151,6 @@ not_balanced_starting_units = {
|
|||||||
item_names.TEMPEST,
|
item_names.TEMPEST,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Defense rating table
|
|
||||||
# Commented defense ratings are handled in LogicMixin
|
|
||||||
tvx_defense_ratings = {
|
|
||||||
item_names.SIEGE_TANK: 5,
|
|
||||||
# "Graduating Range": 1,
|
|
||||||
item_names.PLANETARY_FORTRESS: 3,
|
|
||||||
# Bunker w/ Marine/Marauder: 3,
|
|
||||||
item_names.PERDITION_TURRET: 2,
|
|
||||||
item_names.DEVASTATOR_TURRET: 2,
|
|
||||||
item_names.VULTURE: 1,
|
|
||||||
item_names.BANSHEE: 1,
|
|
||||||
item_names.BATTLECRUISER: 1,
|
|
||||||
item_names.LIBERATOR: 4,
|
|
||||||
item_names.WIDOW_MINE: 1,
|
|
||||||
# "Concealment (Widow Mine)": 1
|
|
||||||
}
|
|
||||||
tvz_defense_ratings = {
|
|
||||||
item_names.PERDITION_TURRET: 2,
|
|
||||||
# Bunker w/ Firebat: 2,
|
|
||||||
item_names.LIBERATOR: -2,
|
|
||||||
item_names.HIVE_MIND_EMULATOR: 3,
|
|
||||||
item_names.PSI_DISRUPTER: 3,
|
|
||||||
}
|
|
||||||
tvx_air_defense_ratings = {
|
|
||||||
item_names.MISSILE_TURRET: 2,
|
|
||||||
}
|
|
||||||
zvx_defense_ratings = {
|
|
||||||
# Note that this doesn't include Kerrigan because this is just for race swaps, which doesn't involve her (for now)
|
|
||||||
item_names.SPINE_CRAWLER: 3,
|
|
||||||
# w/ Twin Drones: 1
|
|
||||||
item_names.SWARM_QUEEN: 1,
|
|
||||||
item_names.SWARM_HOST: 1,
|
|
||||||
# impaler: 3
|
|
||||||
# "Hardened Tentacle Spines (Impaler)": 2
|
|
||||||
# lurker: 1
|
|
||||||
# "Seismic Spines (Lurker)": 2
|
|
||||||
# "Adapted Spines (Lurker)": 1
|
|
||||||
# brood lord : 2
|
|
||||||
# corpser roach: 1
|
|
||||||
# creep tumors (swarm queen or overseer): 1
|
|
||||||
# w/ malignant creep: 1
|
|
||||||
# tanks with ammo: 5
|
|
||||||
item_names.INFESTED_BUNKER: 3,
|
|
||||||
item_names.BILE_LAUNCHER: 2,
|
|
||||||
}
|
|
||||||
# zvz_defense_ratings = {
|
|
||||||
# corpser roach: 1
|
|
||||||
# primal igniter: 2
|
|
||||||
# lurker: 1
|
|
||||||
# w/ adapted spines: -1
|
|
||||||
# impaler: -1
|
|
||||||
# }
|
|
||||||
zvx_air_defense_ratings = {
|
|
||||||
item_names.SPORE_CRAWLER: 2,
|
|
||||||
# w/ Twin Drones: 1
|
|
||||||
item_names.INFESTED_MISSILE_TURRET: 2,
|
|
||||||
}
|
|
||||||
pvx_defense_ratings = {
|
|
||||||
item_names.PHOTON_CANNON: 2,
|
|
||||||
item_names.KHAYDARIN_MONOLITH: 3,
|
|
||||||
item_names.SHIELD_BATTERY: 1,
|
|
||||||
item_names.NEXUS_OVERCHARGE: 2,
|
|
||||||
item_names.SKYLORD: 1,
|
|
||||||
item_names.MATRIX_OVERLOAD: 1,
|
|
||||||
item_names.COLOSSUS: 1,
|
|
||||||
item_names.VANGUARD: 1,
|
|
||||||
item_names.REAVER: 1,
|
|
||||||
}
|
|
||||||
pvz_defense_ratings = {
|
|
||||||
item_names.KHAYDARIN_MONOLITH: -2,
|
|
||||||
item_names.COLOSSUS: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
terran_passive_ratings = {
|
|
||||||
item_names.AUTOMATED_REFINERY: 4,
|
|
||||||
item_names.COMMAND_CENTER_MULE: 4,
|
|
||||||
item_names.ORBITAL_DEPOTS: 2,
|
|
||||||
item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: 2,
|
|
||||||
item_names.COMMAND_CENTER_EXTRA_SUPPLIES: 2,
|
|
||||||
item_names.MICRO_FILTERING: 2,
|
|
||||||
item_names.TECH_REACTOR: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
zerg_passive_ratings = {
|
|
||||||
item_names.TWIN_DRONES: 7,
|
|
||||||
item_names.AUTOMATED_EXTRACTORS: 4,
|
|
||||||
item_names.VESPENE_EFFICIENCY: 3,
|
|
||||||
item_names.OVERLORD_IMPROVED_OVERLORDS: 4,
|
|
||||||
item_names.MALIGNANT_CREEP: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
protoss_passive_ratings = {
|
|
||||||
item_names.QUATRO: 4,
|
|
||||||
item_names.ORBITAL_ASSIMILATORS: 4,
|
|
||||||
item_names.AMPLIFIED_ASSIMILATORS: 3,
|
|
||||||
item_names.PROBE_WARPIN: 2,
|
|
||||||
item_names.ELDER_PROBES: 2,
|
|
||||||
item_names.MATRIX_OVERLOAD: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
soa_energy_ratings = {
|
|
||||||
item_names.SOA_SOLAR_LANCE: 8,
|
|
||||||
item_names.SOA_DEPLOY_FENIX: 7,
|
|
||||||
item_names.SOA_TEMPORAL_FIELD: 6,
|
|
||||||
item_names.SOA_PROGRESSIVE_PROXY_PYLON: 5, # Requires Lvl 2 (Warp in Reinforcements)
|
|
||||||
item_names.SOA_SHIELD_OVERCHARGE: 5,
|
|
||||||
item_names.SOA_ORBITAL_STRIKE: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
soa_passive_ratings = {
|
|
||||||
item_names.GUARDIAN_SHELL: 4,
|
|
||||||
item_names.OVERWATCH: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
soa_ultimate_ratings = {
|
|
||||||
item_names.SOA_TIME_STOP: 4,
|
|
||||||
item_names.SOA_PURIFIER_BEAM: 3,
|
|
||||||
item_names.SOA_SOLAR_BOMBARDMENT: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
kerrigan_levels = [
|
kerrigan_levels = [
|
||||||
item_name for item_name, item_data in item_table.items()
|
item_name for item_name, item_data in item_table.items()
|
||||||
if item_data.type == ZergItemType.Level and item_data.race == SC2Race.ZERG
|
if item_data.type == ZergItemType.Level and item_data.race == SC2Race.ZERG
|
||||||
@@ -2293,12 +2172,6 @@ spear_of_adun_calldowns = {
|
|||||||
item_names.SOA_SOLAR_BOMBARDMENT
|
item_names.SOA_SOLAR_BOMBARDMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
spear_of_adun_castable_passives = {
|
|
||||||
item_names.RECONSTRUCTION_BEAM,
|
|
||||||
item_names.OVERWATCH,
|
|
||||||
item_names.GUARDIAN_SHELL,
|
|
||||||
}
|
|
||||||
|
|
||||||
nova_equipment = {
|
nova_equipment = {
|
||||||
*[item_name for item_name, item_data in get_full_item_list().items()
|
*[item_name for item_name, item_data in get_full_item_list().items()
|
||||||
if item_data.type == TerranItemType.Nova_Gear],
|
if item_data.type == TerranItemType.Nova_Gear],
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from Options import (
|
from Options import (
|
||||||
Choice, Toggle, DefaultOnToggle, OptionSet, Range,
|
Choice, Toggle, DefaultOnToggle, OptionSet, Range,
|
||||||
PerGameCommonOptions, Option, VerifyKeys, StartInventory,
|
PerGameCommonOptions, VerifyKeys, StartInventory,
|
||||||
is_iterable_except_str, OptionGroup, Visibility, ItemDict,
|
is_iterable_except_str, OptionGroup, Visibility, ItemDict,
|
||||||
Accessibility, ProgressionBalancing
|
OptionCounter,
|
||||||
)
|
)
|
||||||
from Utils import get_fuzzy_results
|
from Utils import get_fuzzy_results
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from .item import item_names, item_tables
|
from .item import item_names, item_tables, item_groups
|
||||||
from .item.item_groups import kerrigan_active_abilities, kerrigan_passives, nova_weapons, nova_gadgets
|
|
||||||
from .mission_tables import (
|
from .mission_tables import (
|
||||||
SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list,
|
SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list,
|
||||||
campaign_mission_table, SC2Race, MissionFlag
|
campaign_mission_table, SC2Race, MissionFlag
|
||||||
@@ -700,7 +699,7 @@ class KerriganMaxActiveAbilities(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Kerrigan Maximum Active Abilities"
|
display_name = "Kerrigan Maximum Active Abilities"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = len(kerrigan_active_abilities)
|
range_end = len(item_groups.kerrigan_active_abilities)
|
||||||
default = range_end
|
default = range_end
|
||||||
|
|
||||||
|
|
||||||
@@ -711,7 +710,7 @@ class KerriganMaxPassiveAbilities(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Kerrigan Maximum Passive Abilities"
|
display_name = "Kerrigan Maximum Passive Abilities"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = len(kerrigan_passives)
|
range_end = len(item_groups.kerrigan_passives)
|
||||||
default = range_end
|
default = range_end
|
||||||
|
|
||||||
|
|
||||||
@@ -829,7 +828,7 @@ class SpearOfAdunMaxAutocastAbilities(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Spear of Adun Maximum Passive Abilities"
|
display_name = "Spear of Adun Maximum Passive Abilities"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = sum(item.quantity for item_name, item in item_tables.get_full_item_list().items() if item_name in item_tables.spear_of_adun_castable_passives)
|
range_end = sum(item_tables.item_table[item_name].quantity for item_name in item_groups.spear_of_adun_passives)
|
||||||
default = range_end
|
default = range_end
|
||||||
|
|
||||||
|
|
||||||
@@ -883,7 +882,7 @@ class NovaMaxWeapons(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Nova Maximum Weapons"
|
display_name = "Nova Maximum Weapons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = len(nova_weapons)
|
range_end = len(item_groups.nova_weapons)
|
||||||
default = range_end
|
default = range_end
|
||||||
|
|
||||||
|
|
||||||
@@ -897,7 +896,7 @@ class NovaMaxGadgets(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Nova Maximum Gadgets"
|
display_name = "Nova Maximum Gadgets"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = len(nova_gadgets)
|
range_end = len(item_groups.nova_gadgets)
|
||||||
default = range_end
|
default = range_end
|
||||||
|
|
||||||
|
|
||||||
@@ -932,33 +931,48 @@ class TakeOverAIAllies(Toggle):
|
|||||||
display_name = "Take Over AI Allies"
|
display_name = "Take Over AI Allies"
|
||||||
|
|
||||||
|
|
||||||
class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
|
class Sc2ItemDict(OptionCounter, VerifyKeys, Mapping[str, int]):
|
||||||
"""A branch of ItemDict that supports item counts of 0"""
|
"""A branch of ItemDict that supports negative item counts"""
|
||||||
default = {}
|
default = {}
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
verify_item_name = True
|
verify_item_name = True
|
||||||
# convert_name_groups = True
|
# convert_name_groups = True
|
||||||
display_name = 'Unnamed dictionary'
|
display_name = 'Unnamed dictionary'
|
||||||
minimum_value: int = 0
|
# Note(phaneros): Limiting minimum to -1 means that if two triggers add -1 to the same item,
|
||||||
|
# the validation fails. So give trigger people space to stack a bunch of triggers.
|
||||||
|
min: int = -1000
|
||||||
|
max: int = 1000
|
||||||
|
valid_keys = set(item_tables.item_table) | set(item_groups.item_name_groups)
|
||||||
|
|
||||||
def __init__(self, value: Dict[str, int]):
|
def __init__(self, value: dict[str, int]):
|
||||||
self.value = {key: val for key, val in value.items()}
|
self.value = {key: val for key, val in value.items()}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: Union[List[str], Dict[str, int]]) -> 'Sc2ItemDict':
|
def from_any(cls, data: list[str] | dict[str, int]) -> 'Sc2ItemDict':
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
# This is a little default that gets us backwards compatibility with lists.
|
raise ValueError(
|
||||||
# It doesn't play nice with trigger merging dicts and lists together, though, so best not to advertise it overmuch.
|
f"{cls.display_name}: Cannot convert from list. "
|
||||||
data = {item: 0 for item in data}
|
f"Use dict syntax (no dashes, 'value: number' synax)."
|
||||||
|
)
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if not isinstance(value, int):
|
if not isinstance(value, int):
|
||||||
raise ValueError(f"Invalid type in '{cls.display_name}': element '{key}' maps to '{value}', expected an integer")
|
raise ValueError(
|
||||||
if value < cls.minimum_value:
|
f"Invalid type in '{cls.display_name}': "
|
||||||
raise ValueError(f"Invalid value for '{cls.display_name}': element '{key}' maps to {value}, which is less than the minimum ({cls.minimum_value})")
|
f"element '{key}' maps to '{value}', expected an integer"
|
||||||
|
)
|
||||||
|
if value < cls.min:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid value for '{cls.display_name}': "
|
||||||
|
f"element '{key}' maps to {value}, which is less than the minimum ({cls.min})"
|
||||||
|
)
|
||||||
|
if value > cls.max:
|
||||||
|
raise ValueError(f"Invalid value for '{cls.display_name}': "
|
||||||
|
f"element '{key}' maps to {value}, which is greater than the maximum ({cls.max})"
|
||||||
|
)
|
||||||
return cls(data)
|
return cls(data)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"{cls.display_name}: Cannot convert from non-dictionary, got {type(data)}")
|
||||||
|
|
||||||
def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None:
|
def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None:
|
||||||
"""Overridden version of function from Options.VerifyKeys for a better error message"""
|
"""Overridden version of function from Options.VerifyKeys for a better error message"""
|
||||||
@@ -974,15 +988,16 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
|
|||||||
self.value = new_value
|
self.value = new_value
|
||||||
for item_name in self.value:
|
for item_name in self.value:
|
||||||
if item_name not in world.item_names:
|
if item_name not in world.item_names:
|
||||||
from .item import item_groups
|
|
||||||
picks = get_fuzzy_results(
|
picks = get_fuzzy_results(
|
||||||
item_name,
|
item_name,
|
||||||
list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()),
|
list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()),
|
||||||
limit=1,
|
limit=1,
|
||||||
)
|
)
|
||||||
raise Exception(f"Item {item_name} from option {self} "
|
raise Exception(
|
||||||
|
f"Item {item_name} from option {self} "
|
||||||
f"is not a valid item name from {world.game}. "
|
f"is not a valid item name from {world.game}. "
|
||||||
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)"
|
||||||
|
)
|
||||||
|
|
||||||
def get_option_name(self, value):
|
def get_option_name(self, value):
|
||||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||||
@@ -998,25 +1013,25 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
|
|||||||
|
|
||||||
|
|
||||||
class Sc2StartInventory(Sc2ItemDict):
|
class Sc2StartInventory(Sc2ItemDict):
|
||||||
"""Start with these items."""
|
"""Start with these items. Use an amount of -1 to start with all copies of an item."""
|
||||||
display_name = StartInventory.display_name
|
display_name = StartInventory.display_name
|
||||||
|
|
||||||
|
|
||||||
class LockedItems(Sc2ItemDict):
|
class LockedItems(Sc2ItemDict):
|
||||||
"""Guarantees that these items will be unlockable, in the amount specified.
|
"""Guarantees that these items will be unlockable, in the amount specified.
|
||||||
Specify an amount of 0 to lock all copies of an item."""
|
Specify an amount of -1 to lock all copies of an item."""
|
||||||
display_name = "Locked Items"
|
display_name = "Locked Items"
|
||||||
|
|
||||||
|
|
||||||
class ExcludedItems(Sc2ItemDict):
|
class ExcludedItems(Sc2ItemDict):
|
||||||
"""Guarantees that these items will not be unlockable, in the amount specified.
|
"""Guarantees that these items will not be unlockable, in the amount specified.
|
||||||
Specify an amount of 0 to exclude all copies of an item."""
|
Specify an amount of -1 to exclude all copies of an item."""
|
||||||
display_name = "Excluded Items"
|
display_name = "Excluded Items"
|
||||||
|
|
||||||
|
|
||||||
class UnexcludedItems(Sc2ItemDict):
|
class UnexcludedItems(Sc2ItemDict):
|
||||||
"""Undoes an item exclusion; useful for whitelisting or fine-tuning a category.
|
"""Undoes an item exclusion; useful for whitelisting or fine-tuning a category.
|
||||||
Specify an amount of 0 to unexclude all copies of an item."""
|
Specify an amount of -1 to unexclude all copies of an item."""
|
||||||
display_name = "Unexcluded Items"
|
display_name = "Unexcluded Items"
|
||||||
|
|
||||||
|
|
||||||
@@ -1294,7 +1309,7 @@ class MaximumSupplyReductionPerItem(Range):
|
|||||||
class LowestMaximumSupply(Range):
|
class LowestMaximumSupply(Range):
|
||||||
"""Controls how far max supply reduction traps can reduce maximum supply."""
|
"""Controls how far max supply reduction traps can reduce maximum supply."""
|
||||||
display_name = "Lowest Maximum Supply"
|
display_name = "Lowest Maximum Supply"
|
||||||
range_start = 100
|
range_start = 50
|
||||||
range_end = 200
|
range_end = 200
|
||||||
default = 180
|
default = 180
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ from typing import Callable, Dict, List, Set, Tuple, TYPE_CHECKING, Iterable
|
|||||||
|
|
||||||
from BaseClasses import Location, ItemClassification
|
from BaseClasses import Location, ItemClassification
|
||||||
from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups
|
from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups
|
||||||
from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns, \
|
from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns
|
||||||
spear_of_adun_castable_passives
|
|
||||||
from .options import RequiredTactics
|
from .options import RequiredTactics
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -272,7 +271,7 @@ class ValidInventory:
|
|||||||
self.world.random.shuffle(spear_of_adun_actives)
|
self.world.random.shuffle(spear_of_adun_actives)
|
||||||
cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value)
|
cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value)
|
||||||
|
|
||||||
spear_of_adun_autocasts = [item for item in inventory if item.name in spear_of_adun_castable_passives]
|
spear_of_adun_autocasts = [item for item in inventory if item.name in item_groups.spear_of_adun_passives]
|
||||||
self.world.random.shuffle(spear_of_adun_autocasts)
|
self.world.random.shuffle(spear_of_adun_autocasts)
|
||||||
cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value)
|
cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value)
|
||||||
|
|
||||||
|
|||||||
@@ -19,26 +19,13 @@ from .options import (
|
|||||||
get_enabled_races,
|
get_enabled_races,
|
||||||
)
|
)
|
||||||
from .item.item_tables import (
|
from .item.item_tables import (
|
||||||
tvx_defense_ratings,
|
|
||||||
tvz_defense_ratings,
|
|
||||||
tvx_air_defense_ratings,
|
|
||||||
kerrigan_levels,
|
kerrigan_levels,
|
||||||
get_full_item_list,
|
get_full_item_list,
|
||||||
zvx_air_defense_ratings,
|
|
||||||
zvx_defense_ratings,
|
|
||||||
pvx_defense_ratings,
|
|
||||||
pvz_defense_ratings,
|
|
||||||
no_logic_basic_units,
|
no_logic_basic_units,
|
||||||
advanced_basic_units,
|
advanced_basic_units,
|
||||||
basic_units,
|
basic_units,
|
||||||
upgrade_bundle_inverted_lookup,
|
upgrade_bundle_inverted_lookup,
|
||||||
WEAPON_ARMOR_UPGRADE_MAX_LEVEL,
|
WEAPON_ARMOR_UPGRADE_MAX_LEVEL,
|
||||||
soa_ultimate_ratings,
|
|
||||||
soa_energy_ratings,
|
|
||||||
terran_passive_ratings,
|
|
||||||
soa_passive_ratings,
|
|
||||||
zerg_passive_ratings,
|
|
||||||
protoss_passive_ratings,
|
|
||||||
)
|
)
|
||||||
from .mission_tables import SC2Race, SC2Campaign
|
from .mission_tables import SC2Race, SC2Campaign
|
||||||
from .item import item_groups, item_names
|
from .item import item_groups, item_names
|
||||||
@@ -256,8 +243,7 @@ class SC2Logic:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def terran_any_air_unit(self, state: CollectionState) -> bool:
|
def terran_any_air_unit(self, state: CollectionState) -> bool:
|
||||||
return state.has_any(
|
return state.has_any((
|
||||||
{
|
|
||||||
item_names.VIKING,
|
item_names.VIKING,
|
||||||
item_names.MEDIVAC,
|
item_names.MEDIVAC,
|
||||||
item_names.RAVEN,
|
item_names.RAVEN,
|
||||||
@@ -273,9 +259,7 @@ class SC2Logic:
|
|||||||
item_names.EMPERORS_GUARDIAN,
|
item_names.EMPERORS_GUARDIAN,
|
||||||
item_names.NIGHT_WOLF,
|
item_names.NIGHT_WOLF,
|
||||||
item_names.PRIDE_OF_AUGUSTRGRAD,
|
item_names.PRIDE_OF_AUGUSTRGRAD,
|
||||||
},
|
), self.player)
|
||||||
self.player,
|
|
||||||
)
|
|
||||||
|
|
||||||
def terran_competent_ground_to_air(self, state: CollectionState) -> bool:
|
def terran_competent_ground_to_air(self, state: CollectionState) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -288,12 +272,14 @@ class SC2Logic:
|
|||||||
and self.terran_bio_heal(state)
|
and self.terran_bio_heal(state)
|
||||||
and self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state) >= 2
|
and self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state) >= 2
|
||||||
)
|
)
|
||||||
or self.advanced_tactics
|
or (
|
||||||
|
self.advanced_tactics
|
||||||
and (
|
and (
|
||||||
state.has(item_names.CYCLONE, self.player)
|
state.has(item_names.CYCLONE, self.player)
|
||||||
or state.has_all((item_names.THOR, item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD), self.player)
|
or state.has_all((item_names.THOR, item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD), self.player)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def terran_competent_anti_air(self, state: CollectionState) -> bool:
|
def terran_competent_anti_air(self, state: CollectionState) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -381,8 +367,7 @@ class SC2Logic:
|
|||||||
Basic AA to deal with few air units
|
Basic AA to deal with few air units
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
state.has_any(
|
state.has_any((
|
||||||
(
|
|
||||||
item_names.MISSILE_TURRET,
|
item_names.MISSILE_TURRET,
|
||||||
item_names.WAR_PIGS,
|
item_names.WAR_PIGS,
|
||||||
item_names.SPARTAN_COMPANY,
|
item_names.SPARTAN_COMPANY,
|
||||||
@@ -392,22 +377,18 @@ class SC2Logic:
|
|||||||
item_names.SKY_FURY,
|
item_names.SKY_FURY,
|
||||||
item_names.SON_OF_KORHAL,
|
item_names.SON_OF_KORHAL,
|
||||||
item_names.BULWARK_COMPANY,
|
item_names.BULWARK_COMPANY,
|
||||||
),
|
), self.player)
|
||||||
self.player,
|
|
||||||
)
|
|
||||||
or self.terran_moderate_anti_air(state)
|
or self.terran_moderate_anti_air(state)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and (
|
and (
|
||||||
state.has_any(
|
state.has_any((
|
||||||
(
|
|
||||||
item_names.WIDOW_MINE,
|
item_names.WIDOW_MINE,
|
||||||
item_names.PRIDE_OF_AUGUSTRGRAD,
|
item_names.PRIDE_OF_AUGUSTRGRAD,
|
||||||
item_names.BLACKHAMMER,
|
item_names.BLACKHAMMER,
|
||||||
item_names.EMPERORS_SHADOW,
|
item_names.EMPERORS_SHADOW,
|
||||||
item_names.EMPERORS_GUARDIAN,
|
item_names.EMPERORS_GUARDIAN,
|
||||||
item_names.NIGHT_HAWK,
|
item_names.NIGHT_HAWK,
|
||||||
),
|
), self.player)
|
||||||
self.player,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -422,30 +403,37 @@ class SC2Logic:
|
|||||||
"""
|
"""
|
||||||
defense_score = sum((tvx_defense_ratings[item] for item in tvx_defense_ratings if state.has(item, self.player)))
|
defense_score = sum((tvx_defense_ratings[item] for item in tvx_defense_ratings if state.has(item, self.player)))
|
||||||
# Manned Bunker
|
# Manned Bunker
|
||||||
if state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player) and state.has(
|
if (state.has_any((item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER), self.player)
|
||||||
item_names.BUNKER, self.player
|
and state.has(item_names.BUNKER, self.player)
|
||||||
):
|
):
|
||||||
defense_score += 3
|
defense_score += 3
|
||||||
elif zerg_enemy and state.has(item_names.FIREBAT, self.player) and state.has(item_names.BUNKER, self.player):
|
elif zerg_enemy and state.has(item_names.FIREBAT, self.player) and state.has(item_names.BUNKER, self.player):
|
||||||
defense_score += 2
|
defense_score += 2
|
||||||
# Siege Tank upgrades
|
# Siege Tank upgrades
|
||||||
if state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_MAELSTROM_ROUNDS}, self.player):
|
if state.has_all((item_names.SIEGE_TANK, item_names.SIEGE_TANK_MAELSTROM_ROUNDS), self.player):
|
||||||
defense_score += 2
|
defense_score += 2
|
||||||
if state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_GRADUATING_RANGE}, self.player):
|
if state.has_all((item_names.SIEGE_TANK, item_names.SIEGE_TANK_GRADUATING_RANGE), self.player):
|
||||||
defense_score += 1
|
defense_score += 1
|
||||||
# Widow Mine upgrade
|
# Widow Mine upgrade
|
||||||
if state.has_all({item_names.WIDOW_MINE, item_names.WIDOW_MINE_CONCEALMENT}, self.player):
|
if state.has_all((item_names.WIDOW_MINE, item_names.WIDOW_MINE_CONCEALMENT), self.player):
|
||||||
defense_score += 1
|
defense_score += 1
|
||||||
# Viking with splash
|
# Viking with splash
|
||||||
if state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player):
|
if state.has_all((item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS), self.player):
|
||||||
defense_score += 2
|
defense_score += 2
|
||||||
|
|
||||||
# General enemy-based rules
|
# General enemy-based rules
|
||||||
if zerg_enemy:
|
if zerg_enemy:
|
||||||
defense_score += sum((tvz_defense_ratings[item] for item in tvz_defense_ratings if state.has(item, self.player)))
|
defense_score += sum((
|
||||||
|
tvz_defense_ratings[item]
|
||||||
|
for item in tvz_defense_ratings
|
||||||
|
if state.has(item, self.player)
|
||||||
|
))
|
||||||
if air_enemy:
|
if air_enemy:
|
||||||
# Capped at 2
|
# Capped at 2
|
||||||
defense_score += min(sum((tvx_air_defense_ratings[item] for item in tvx_air_defense_ratings if state.has(item, self.player))), 2)
|
defense_score += min2(
|
||||||
|
2,
|
||||||
|
sum((tvx_air_defense_ratings[item] for item in tvx_air_defense_ratings if state.has(item, self.player))),
|
||||||
|
)
|
||||||
if air_enemy and zerg_enemy and state.has(item_names.VALKYRIE, self.player):
|
if air_enemy and zerg_enemy and state.has(item_names.VALKYRIE, self.player):
|
||||||
# Valkyries shred mass Mutas, the most common air enemy that's massed in these cases
|
# Valkyries shred mass Mutas, the most common air enemy that's massed in these cases
|
||||||
defense_score += 2
|
defense_score += 2
|
||||||
@@ -461,20 +449,25 @@ class SC2Logic:
|
|||||||
# Infantry with Healing
|
# Infantry with Healing
|
||||||
infantry_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state)
|
infantry_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state)
|
||||||
infantry_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, state)
|
infantry_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, state)
|
||||||
infantry = state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player)
|
infantry = state.has_any((item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER), self.player)
|
||||||
if infantry_weapons >= upgrade_level + 1 and infantry_armor >= upgrade_level and infantry and self.terran_bio_heal(state):
|
if (infantry_weapons >= upgrade_level + 1
|
||||||
|
and infantry_armor >= upgrade_level
|
||||||
|
and infantry
|
||||||
|
and self.terran_bio_heal(state)
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
# Mass Air-To-Ground
|
# Mass Air-To-Ground
|
||||||
ship_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state)
|
ship_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state)
|
||||||
ship_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, state)
|
ship_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, state)
|
||||||
if ship_weapons >= upgrade_level and ship_armor >= upgrade_level:
|
if ship_weapons >= upgrade_level and ship_armor >= upgrade_level:
|
||||||
air = (
|
air = (
|
||||||
state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player)
|
state.has_any((item_names.BANSHEE, item_names.BATTLECRUISER), self.player)
|
||||||
or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player)
|
or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player)
|
||||||
or state.has_all({item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
|
or state.has_all((item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY), self.player)
|
||||||
or state.has_all({item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES}, self.player)
|
or (state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player)
|
||||||
and ship_weapons >= 2
|
and ship_weapons >= 2
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if air and self.terran_mineral_dump(state):
|
if air and self.terran_mineral_dump(state):
|
||||||
return True
|
return True
|
||||||
# Strong Mech
|
# Strong Mech
|
||||||
@@ -499,9 +492,11 @@ class SC2Logic:
|
|||||||
Can build something using only minerals
|
Can build something using only minerals
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
state.has_any({item_names.MARINE, item_names.VULTURE, item_names.HELLION, item_names.SON_OF_KORHAL}, self.player)
|
state.has_any((item_names.MARINE, item_names.VULTURE, item_names.HELLION, item_names.SON_OF_KORHAL), self.player)
|
||||||
or state.has_all({item_names.REAPER, item_names.REAPER_RESOURCE_EFFICIENCY}, self.player)
|
or state.has_all((item_names.REAPER, item_names.REAPER_RESOURCE_EFFICIENCY), self.player)
|
||||||
or (self.advanced_tactics and state.has_any({item_names.PERDITION_TURRET, item_names.DEVASTATOR_TURRET}, self.player))
|
or (self.advanced_tactics
|
||||||
|
and state.has_any((item_names.PERDITION_TURRET, item_names.DEVASTATOR_TURRET), self.player)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def terran_beats_protoss_deathball(self, state: CollectionState) -> bool:
|
def terran_beats_protoss_deathball(self, state: CollectionState) -> bool:
|
||||||
@@ -510,12 +505,15 @@ class SC2Logic:
|
|||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player)
|
(
|
||||||
or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player)
|
state.has_any((item_names.BANSHEE, item_names.BATTLECRUISER), self.player)
|
||||||
|
or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player)
|
||||||
)
|
)
|
||||||
and self.terran_competent_anti_air(state)
|
and self.terran_competent_anti_air(state)
|
||||||
or self.terran_competent_comp(state)
|
)
|
||||||
|
or (self.terran_competent_comp(state)
|
||||||
and self.terran_air_anti_air(state)
|
and self.terran_air_anti_air(state)
|
||||||
|
)
|
||||||
) and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2
|
) and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2
|
||||||
|
|
||||||
def marine_medic_upgrade(self, state: CollectionState) -> bool:
|
def marine_medic_upgrade(self, state: CollectionState) -> bool:
|
||||||
@@ -523,11 +521,18 @@ class SC2Logic:
|
|||||||
Infantry upgrade to infantry-only no-build segments
|
Infantry upgrade to infantry-only no-build segments
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
state.has_any({item_names.MARINE_COMBAT_SHIELD, item_names.MARINE_MAGRAIL_MUNITIONS, item_names.MEDIC_STABILIZER_MEDPACKS}, self.player)
|
state.has_any((
|
||||||
or (state.count(item_names.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2 and state.has_group("Missions", self.player, 1))
|
item_names.MARINE_COMBAT_SHIELD,
|
||||||
or self.advanced_tactics
|
item_names.MARINE_MAGRAIL_MUNITIONS,
|
||||||
|
item_names.MEDIC_STABILIZER_MEDPACKS,
|
||||||
|
), self.player)
|
||||||
|
or (state.count(item_names.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2
|
||||||
|
and state.has_group("Missions", self.player, 1)
|
||||||
|
)
|
||||||
|
or (self.advanced_tactics
|
||||||
and state.has(item_names.MARINE_LASER_TARGETING_SYSTEM, self.player)
|
and state.has(item_names.MARINE_LASER_TARGETING_SYSTEM, self.player)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def marine_medic_firebat_upgrade(self, state: CollectionState) -> bool:
|
def marine_medic_firebat_upgrade(self, state: CollectionState) -> bool:
|
||||||
return (
|
return (
|
||||||
@@ -977,15 +982,22 @@ class SC2Logic:
|
|||||||
if self.zerg_army_weapon_armor_upgrade_min_level(state) < 2:
|
if self.zerg_army_weapon_armor_upgrade_min_level(state) < 2:
|
||||||
return False
|
return False
|
||||||
advanced = self.advanced_tactics
|
advanced = self.advanced_tactics
|
||||||
core_unit = state.has_any(
|
core_unit = (
|
||||||
{item_names.ROACH, item_names.ABERRATION, item_names.ZERGLING, item_names.INFESTED_DIAMONDBACK}, self.player
|
state.has_any((
|
||||||
) or self.morph_igniter(state)
|
item_names.ROACH,
|
||||||
|
item_names.ABERRATION,
|
||||||
|
item_names.ZERGLING,
|
||||||
|
item_names.INFESTED_DIAMONDBACK,
|
||||||
|
), self.player)
|
||||||
|
or self.morph_igniter(state)
|
||||||
|
)
|
||||||
support_unit = (
|
support_unit = (
|
||||||
state.has_any({item_names.SWARM_QUEEN, item_names.HYDRALISK, item_names.INFESTED_BANSHEE}, self.player)
|
state.has_any({item_names.SWARM_QUEEN, item_names.HYDRALISK, item_names.INFESTED_BANSHEE}, self.player)
|
||||||
or self.morph_brood_lord(state)
|
or self.morph_brood_lord(state)
|
||||||
or state.has_all((item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player)
|
or state.has_all((item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player)
|
||||||
or advanced
|
or (advanced
|
||||||
and (state.has_any({item_names.INFESTOR, item_names.DEFILER}, self.player) or self.morph_viper(state))
|
and (state.has_any((item_names.INFESTOR, item_names.DEFILER), self.player) or self.morph_viper(state))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if core_unit and support_unit:
|
if core_unit and support_unit:
|
||||||
return True
|
return True
|
||||||
@@ -997,9 +1009,10 @@ class SC2Logic:
|
|||||||
(item_names.GUARDIAN_SORONAN_ACID, item_names.GUARDIAN_EXPLOSIVE_SPORES, item_names.GUARDIAN_PRIMORDIAL_FURY), self.player
|
(item_names.GUARDIAN_SORONAN_ACID, item_names.GUARDIAN_EXPLOSIVE_SPORES, item_names.GUARDIAN_PRIMORDIAL_FURY), self.player
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or advanced
|
or (advanced
|
||||||
and self.morph_viper(state)
|
and self.morph_viper(state)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
return vespene_unit and state.has_any({item_names.ZERGLING, item_names.SWARM_QUEEN}, self.player)
|
return vespene_unit and state.has_any({item_names.ZERGLING, item_names.SWARM_QUEEN}, self.player)
|
||||||
|
|
||||||
def zerg_common_unit_basic_aa(self, state: CollectionState) -> bool:
|
def zerg_common_unit_basic_aa(self, state: CollectionState) -> bool:
|
||||||
@@ -1057,22 +1070,22 @@ class SC2Logic:
|
|||||||
self.zerg_ranged_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level()
|
self.zerg_ranged_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level()
|
||||||
and (
|
and (
|
||||||
self.morph_impaler(state)
|
self.morph_impaler(state)
|
||||||
or self.morph_lurker(state)
|
or (self.morph_lurker(state)
|
||||||
and state.has_all((item_names.LURKER_SEISMIC_SPINES, item_names.LURKER_ADAPTED_SPINES), self.player)
|
and state.has_all((item_names.LURKER_SEISMIC_SPINES, item_names.LURKER_ADAPTED_SPINES), self.player)
|
||||||
or state.has_all(
|
)
|
||||||
(
|
or state.has_all((
|
||||||
item_names.ROACH,
|
item_names.ROACH,
|
||||||
item_names.ROACH_CORPSER_STRAIN,
|
item_names.ROACH_CORPSER_STRAIN,
|
||||||
item_names.ROACH_ADAPTIVE_PLATING,
|
item_names.ROACH_ADAPTIVE_PLATING,
|
||||||
item_names.ROACH_GLIAL_RECONSTITUTION,
|
item_names.ROACH_GLIAL_RECONSTITUTION,
|
||||||
),
|
), self.player)
|
||||||
self.player,
|
or (self.morph_igniter(state)
|
||||||
)
|
|
||||||
or self.morph_igniter(state)
|
|
||||||
and state.has(item_names.PRIMAL_IGNITER_PRIMAL_TENACITY, self.player)
|
and state.has(item_names.PRIMAL_IGNITER_PRIMAL_TENACITY, self.player)
|
||||||
|
)
|
||||||
or state.has_all((item_names.INFESTOR, item_names.INFESTOR_INFESTED_TERRAN), self.player)
|
or state.has_all((item_names.INFESTOR, item_names.INFESTOR_INFESTED_TERRAN), self.player)
|
||||||
or self.spread_creep(state, False)
|
or (self.spread_creep(state, False)
|
||||||
and state.has(item_names.INFESTED_BUNKER, self.player)
|
and state.has(item_names.INFESTED_BUNKER, self.player)
|
||||||
|
)
|
||||||
or self.zerg_infested_tank_with_ammo(state)
|
or self.zerg_infested_tank_with_ammo(state)
|
||||||
# Highly-upgraded swarm hosts may also work, but that would require promoting many upgrades to progression
|
# Highly-upgraded swarm hosts may also work, but that would require promoting many upgrades to progression
|
||||||
)
|
)
|
||||||
@@ -1081,8 +1094,9 @@ class SC2Logic:
|
|||||||
self.zerg_flyer_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level()
|
self.zerg_flyer_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level()
|
||||||
and (
|
and (
|
||||||
self.morph_brood_lord(state)
|
self.morph_brood_lord(state)
|
||||||
or self.morph_guardian(state)
|
or (self.morph_guardian(state)
|
||||||
and state.has_all((item_names.GUARDIAN_PROPELLANT_SACS, item_names.GUARDIAN_SORONAN_ACID), self.player)
|
and state.has_all((item_names.GUARDIAN_PROPELLANT_SACS, item_names.GUARDIAN_SORONAN_ACID), self.player)
|
||||||
|
)
|
||||||
or state.has_all((item_names.INFESTED_BANSHEE, item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS), self.player)
|
or state.has_all((item_names.INFESTED_BANSHEE, item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS), self.player)
|
||||||
# Highly-upgraded anti-ground devourers would also be good
|
# Highly-upgraded anti-ground devourers would also be good
|
||||||
)
|
)
|
||||||
@@ -1154,11 +1168,7 @@ class SC2Logic:
|
|||||||
def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool:
|
def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool:
|
||||||
if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant:
|
if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant:
|
||||||
return True
|
return True
|
||||||
count = 0
|
return state.count_from_list(item_groups.kerrigan_logic_active_abilities, self.player) >= 2
|
||||||
for i in range(7):
|
|
||||||
if state.has_any(kerrigan_logic_active_abilities, self.player):
|
|
||||||
count += 1
|
|
||||||
return count >= 2
|
|
||||||
|
|
||||||
# Global Protoss
|
# Global Protoss
|
||||||
def protoss_power_rating(self, state: CollectionState) -> int:
|
def protoss_power_rating(self, state: CollectionState) -> int:
|
||||||
@@ -1346,7 +1356,6 @@ class SC2Logic:
|
|||||||
item_names.MISTWING,
|
item_names.MISTWING,
|
||||||
item_names.CALADRIUS,
|
item_names.CALADRIUS,
|
||||||
item_names.OPPRESSOR,
|
item_names.OPPRESSOR,
|
||||||
item_names.PULSAR,
|
|
||||||
item_names.DRAGOON,
|
item_names.DRAGOON,
|
||||||
},
|
},
|
||||||
self.player,
|
self.player,
|
||||||
@@ -1354,8 +1363,14 @@ class SC2Logic:
|
|||||||
or state.has_all({item_names.TRIREME, item_names.TRIREME_SOLAR_BEAM}, self.player)
|
or state.has_all({item_names.TRIREME, item_names.TRIREME_SOLAR_BEAM}, self.player)
|
||||||
or state.has_all({item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING}, self.player)
|
or state.has_all({item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING}, self.player)
|
||||||
or state.has_all({item_names.WARP_PRISM, item_names.WARP_PRISM_PHASE_BLASTER}, self.player)
|
or state.has_all({item_names.WARP_PRISM, item_names.WARP_PRISM_PHASE_BLASTER}, self.player)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and state.has_any({item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.SENTRY, item_names.ENERGIZER}, self.player)
|
and state.has_any((
|
||||||
|
item_names.HIGH_TEMPLAR,
|
||||||
|
item_names.SIGNIFIER,
|
||||||
|
item_names.SENTRY,
|
||||||
|
item_names.ENERGIZER,
|
||||||
|
), self.player)
|
||||||
|
)
|
||||||
or self.protoss_can_merge_archon(state)
|
or self.protoss_can_merge_archon(state)
|
||||||
or self.protoss_can_merge_dark_archon(state)
|
or self.protoss_can_merge_dark_archon(state)
|
||||||
)
|
)
|
||||||
@@ -1629,9 +1644,10 @@ class SC2Logic:
|
|||||||
return (
|
return (
|
||||||
state.has_any((item_names.ZEALOT, item_names.SENTINEL, item_names.PHOTON_CANNON), self.player)
|
state.has_any((item_names.ZEALOT, item_names.SENTINEL, item_names.PHOTON_CANNON), self.player)
|
||||||
or state.has_all((item_names.CENTURION, item_names.CENTURION_RESOURCE_EFFICIENCY), self.player)
|
or state.has_all((item_names.CENTURION, item_names.CENTURION_RESOURCE_EFFICIENCY), self.player)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and state.has_any((item_names.SUPPLICANT, item_names.SHIELD_BATTERY), self.player)
|
and state.has_any((item_names.SUPPLICANT, item_names.SHIELD_BATTERY), self.player)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def zealot_sentry_slayer_start(self, state: CollectionState) -> bool:
|
def zealot_sentry_slayer_start(self, state: CollectionState) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -1770,8 +1786,12 @@ class SC2Logic:
|
|||||||
return self.zerg_havens_fall_requirement(state) and (
|
return self.zerg_havens_fall_requirement(state) and (
|
||||||
self.morph_devourer(state)
|
self.morph_devourer(state)
|
||||||
or state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player)
|
or state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and (self.morph_viper(state) or state.has_any({item_names.BROOD_QUEEN, item_names.SCOURGE}, self.player))
|
and (
|
||||||
|
self.morph_viper(state)
|
||||||
|
or state.has_any((item_names.BROOD_QUEEN, item_names.SCOURGE), self.player)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def protoss_havens_fall_requirement(self, state: CollectionState) -> bool:
|
def protoss_havens_fall_requirement(self, state: CollectionState) -> bool:
|
||||||
@@ -1780,12 +1800,12 @@ class SC2Logic:
|
|||||||
and self.protoss_competent_anti_air(state)
|
and self.protoss_competent_anti_air(state)
|
||||||
and (
|
and (
|
||||||
self.protoss_competent_comp(state)
|
self.protoss_competent_comp(state)
|
||||||
or (
|
or state.has_any((item_names.TEMPEST, item_names.SKYLORD, item_names.DESTROYER), self.player)
|
||||||
state.has_any((item_names.TEMPEST, item_names.SKYLORD, item_names.DESTROYER), self.player)
|
|
||||||
or (
|
or (
|
||||||
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, state) >= 2
|
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, state) >= 2
|
||||||
and state.has(item_names.CARRIER, self.player)
|
and (
|
||||||
or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player)
|
state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player)
|
||||||
|
or (state.has(item_names.CARRIER, self.player))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1795,30 +1815,33 @@ class SC2Logic:
|
|||||||
"""
|
"""
|
||||||
Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission
|
Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission
|
||||||
"""
|
"""
|
||||||
return self.protoss_havens_fall_requirement(state) and (
|
return (
|
||||||
state.has_any({item_names.CARRIER, item_names.SKYLORD, item_names.DESTROYER, item_names.TEMPEST}, self.player)
|
self.protoss_havens_fall_requirement(state)
|
||||||
# handle mutas
|
and (
|
||||||
|
# One-unit solutions
|
||||||
|
state.has_any((
|
||||||
|
item_names.CARRIER,
|
||||||
|
item_names.SKYLORD,
|
||||||
|
item_names.DESTROYER,
|
||||||
|
item_names.TEMPEST,
|
||||||
|
item_names.VOID_RAY,
|
||||||
|
item_names.SCOUT,
|
||||||
|
), self.player)
|
||||||
or (
|
or (
|
||||||
state.has_any(
|
(
|
||||||
{
|
# handle mutas
|
||||||
|
state.has_any((
|
||||||
item_names.PHOENIX,
|
item_names.PHOENIX,
|
||||||
item_names.MIRAGE,
|
item_names.MIRAGE,
|
||||||
item_names.CORSAIR,
|
item_names.CORSAIR,
|
||||||
},
|
), self.player)
|
||||||
self.player,
|
|
||||||
)
|
|
||||||
or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player)
|
or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player)
|
||||||
)
|
)
|
||||||
# handle brood lords and virophages
|
|
||||||
and (
|
and (
|
||||||
state.has_any(
|
# handle brood lords and virophages
|
||||||
{
|
state.has(item_names.MISTWING, self.player)
|
||||||
item_names.VOID_RAY,
|
)
|
||||||
},
|
|
||||||
self.player,
|
|
||||||
)
|
)
|
||||||
or self.advanced_tactics
|
|
||||||
and state.has_all({item_names.SCOUT, item_names.MISTWING}, self.player)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1876,8 +1899,7 @@ class SC2Logic:
|
|||||||
Able to shoot by a long range or from air to claim the rock formation separated by a chasm
|
Able to shoot by a long range or from air to claim the rock formation separated by a chasm
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
state.has_any(
|
state.has_any((
|
||||||
{
|
|
||||||
item_names.MEDIVAC,
|
item_names.MEDIVAC,
|
||||||
item_names.HERCULES,
|
item_names.HERCULES,
|
||||||
item_names.VIKING,
|
item_names.VIKING,
|
||||||
@@ -1889,9 +1911,7 @@ class SC2Logic:
|
|||||||
item_names.NIGHT_WOLF,
|
item_names.NIGHT_WOLF,
|
||||||
item_names.SHOCK_DIVISION,
|
item_names.SHOCK_DIVISION,
|
||||||
item_names.SKY_FURY,
|
item_names.SKY_FURY,
|
||||||
},
|
), self.player)
|
||||||
self.player,
|
|
||||||
)
|
|
||||||
or state.has_all({item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES}, self.player)
|
or state.has_all({item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES}, self.player)
|
||||||
or state.has_all({item_names.RAVEN, item_names.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
|
or state.has_all({item_names.RAVEN, item_names.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
|
||||||
or (
|
or (
|
||||||
@@ -1901,20 +1921,19 @@ class SC2Logic:
|
|||||||
or (
|
or (
|
||||||
self.advanced_tactics
|
self.advanced_tactics
|
||||||
and (
|
and (
|
||||||
state.has_any(
|
state.has_any((
|
||||||
{
|
|
||||||
item_names.HELS_ANGELS,
|
item_names.HELS_ANGELS,
|
||||||
item_names.DUSK_WINGS,
|
item_names.DUSK_WINGS,
|
||||||
item_names.WINGED_NIGHTMARES,
|
item_names.WINGED_NIGHTMARES,
|
||||||
item_names.SIEGE_BREAKERS,
|
item_names.SIEGE_BREAKERS,
|
||||||
item_names.BRYNHILDS,
|
item_names.BRYNHILDS,
|
||||||
item_names.JACKSONS_REVENGE,
|
item_names.JACKSONS_REVENGE,
|
||||||
},
|
), self.player)
|
||||||
self.player,
|
or state.has_all((
|
||||||
|
item_names.MIDNIGHT_RIDERS, item_names.LIBERATOR_RAID_ARTILLERY,
|
||||||
|
), self.player)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or state.has_all({item_names.MIDNIGHT_RIDERS, item_names.LIBERATOR_RAID_ARTILLERY}, self.player)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def terran_great_train_robbery_train_stopper(self, state: CollectionState) -> bool:
|
def terran_great_train_robbery_train_stopper(self, state: CollectionState) -> bool:
|
||||||
@@ -1947,15 +1966,9 @@ class SC2Logic:
|
|||||||
)
|
)
|
||||||
or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player)
|
or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player)
|
||||||
or state.has_all((item_names.HYDRALISK, item_names.HYDRALISK_MUSCULAR_AUGMENTS), self.player)
|
or state.has_all((item_names.HYDRALISK, item_names.HYDRALISK_MUSCULAR_AUGMENTS), self.player)
|
||||||
or (
|
# Note: Zerglings were tested by Snarky, and it was found they'd need >= 3 upgrades to be viable,
|
||||||
state.has(item_names.ZERGLING, self.player)
|
# so they are not included in this logic.
|
||||||
and (
|
# Raptor + 2 of (Shredding, Adrenal, +2 attack upgrade)
|
||||||
state.has_any(
|
|
||||||
(item_names.ZERGLING_SHREDDING_CLAWS, item_names.ZERGLING_SHREDDING_CLAWS, item_names.ZERGLING_RAPTOR_STRAIN), self.player
|
|
||||||
)
|
|
||||||
)
|
|
||||||
and (self.advanced_tactics or state.has_any((item_names.ZERGLING_METABOLIC_BOOST, item_names.ZERGLING_RAPTOR_STRAIN), self.player))
|
|
||||||
)
|
|
||||||
or self.zerg_infested_tank_with_ammo(state)
|
or self.zerg_infested_tank_with_ammo(state)
|
||||||
or (self.advanced_tactics and (self.morph_tyrannozor(state)))
|
or (self.advanced_tactics and (self.morph_tyrannozor(state)))
|
||||||
)
|
)
|
||||||
@@ -1965,13 +1978,18 @@ class SC2Logic:
|
|||||||
Ability to deal with trains (moving target with a lot of HP)
|
Ability to deal with trains (moving target with a lot of HP)
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
state.has_any(
|
state.has_any((
|
||||||
(item_names.ANNIHILATOR, item_names.IMMORTAL, item_names.STALKER, item_names.WRATHWALKER, item_names.VOID_RAY, item_names.DESTROYER),
|
item_names.ANNIHILATOR,
|
||||||
self.player,
|
item_names.IMMORTAL,
|
||||||
)
|
item_names.STALKER,
|
||||||
or state.has_all({item_names.SLAYER, item_names.SLAYER_PHASE_BLINK}, self.player)
|
item_names.ADEPT, # Tested by Snarky, "An easy 1-item solve"
|
||||||
|
item_names.WRATHWALKER,
|
||||||
|
item_names.VOID_RAY,
|
||||||
|
item_names.DESTROYER,
|
||||||
|
), self.player)
|
||||||
|
or state.has_all((item_names.SLAYER, item_names.SLAYER_PHASE_BLINK), self.player)
|
||||||
or state.has_all((item_names.REAVER, item_names.REAVER_KHALAI_REPLICATORS), self.player)
|
or state.has_all((item_names.REAVER, item_names.REAVER_KHALAI_REPLICATORS), self.player)
|
||||||
or state.has_all({item_names.VANGUARD, item_names.VANGUARD_FUSION_MORTARS}, self.player)
|
or state.has_all((item_names.VANGUARD, item_names.VANGUARD_FUSION_MORTARS), self.player)
|
||||||
or (
|
or (
|
||||||
state.has(item_names.INSTIGATOR, self.player)
|
state.has(item_names.INSTIGATOR, self.player)
|
||||||
and state.has_any((item_names.INSTIGATOR_BLINK_OVERDRIVE, item_names.INSTIGATOR_MODERNIZED_SERVOS), self.player)
|
and state.has_any((item_names.INSTIGATOR_BLINK_OVERDRIVE, item_names.INSTIGATOR_MODERNIZED_SERVOS), self.player)
|
||||||
@@ -1982,8 +2000,7 @@ class SC2Logic:
|
|||||||
self.advanced_tactics
|
self.advanced_tactics
|
||||||
and (
|
and (
|
||||||
state.has(item_names.TEMPEST, self.player)
|
state.has(item_names.TEMPEST, self.player)
|
||||||
or state.has_all((item_names.ADEPT, item_names.ADEPT_RESONATING_GLAIVES), self.player)
|
or state.has_all((item_names.VANGUARD, item_names.VANGUARD_RAPIDFIRE_CANNON), self.player)
|
||||||
or state.has_all({item_names.VANGUARD, item_names.VANGUARD_RAPIDFIRE_CANNON}, self.player)
|
|
||||||
or state.has_all((item_names.OPPRESSOR, item_names.SCOUT_GRAVITIC_THRUSTERS, item_names.OPPRESSOR_VULCAN_BLASTER), self.player)
|
or state.has_all((item_names.OPPRESSOR, item_names.SCOUT_GRAVITIC_THRUSTERS, item_names.OPPRESSOR_VULCAN_BLASTER), self.player)
|
||||||
or state.has_all((item_names.ASCENDANT, item_names.ASCENDANT_POWER_OVERWHELMING, item_names.SUPPLICANT), self.player)
|
or state.has_all((item_names.ASCENDANT, item_names.ASCENDANT_POWER_OVERWHELMING, item_names.SUPPLICANT), self.player)
|
||||||
or state.has_all(
|
or state.has_all(
|
||||||
@@ -2005,7 +2022,12 @@ class SC2Logic:
|
|||||||
"""
|
"""
|
||||||
Rescuing in The Moebius Factor
|
Rescuing in The Moebius Factor
|
||||||
"""
|
"""
|
||||||
return state.has_any({item_names.MEDIVAC, item_names.HERCULES, item_names.RAVEN, item_names.VIKING}, self.player) or self.advanced_tactics
|
return (
|
||||||
|
state.has_any((
|
||||||
|
item_names.MEDIVAC, item_names.HERCULES, item_names.RAVEN, item_names.VIKING
|
||||||
|
), self.player)
|
||||||
|
or self.advanced_tactics
|
||||||
|
)
|
||||||
|
|
||||||
def terran_supernova_requirement(self, state) -> bool:
|
def terran_supernova_requirement(self, state) -> bool:
|
||||||
return self.terran_beats_protoss_deathball(state) and self.terran_power_rating(state) >= 6
|
return self.terran_beats_protoss_deathball(state) and self.terran_power_rating(state) >= 6
|
||||||
@@ -2024,7 +2046,10 @@ class SC2Logic:
|
|||||||
or (self.advanced_tactics and state.has(item_names.PROGRESSIVE_WARP_RELOCATE, self.player))
|
or (self.advanced_tactics and state.has(item_names.PROGRESSIVE_WARP_RELOCATE, self.player))
|
||||||
)
|
)
|
||||||
and self.protoss_competent_anti_air(state)
|
and self.protoss_competent_anti_air(state)
|
||||||
and (self.protoss_fleet(state) or (self.protoss_competent_comp(state) and self.protoss_power_rating(state) >= 6))
|
and (
|
||||||
|
self.protoss_fleet(state)
|
||||||
|
or (self.protoss_competent_comp(state) and self.protoss_power_rating(state) >= 6)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def terran_maw_requirement(self, state: CollectionState) -> bool:
|
def terran_maw_requirement(self, state: CollectionState) -> bool:
|
||||||
@@ -2079,18 +2104,25 @@ class SC2Logic:
|
|||||||
return True
|
return True
|
||||||
usable_muta = (
|
usable_muta = (
|
||||||
state.has_all((item_names.MUTALISK, item_names.MUTALISK_RAPID_REGENERATION), self.player)
|
state.has_all((item_names.MUTALISK, item_names.MUTALISK_RAPID_REGENERATION), self.player)
|
||||||
and state.has_any((item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player)
|
and state.count_from_list_unique((
|
||||||
and (
|
item_names.MUTALISK_SEVERING_GLAIVE,
|
||||||
state.has(item_names.MUTALISK_SUNDERING_GLAIVE, self.player)
|
item_names.MUTALISK_SUNDERING_GLAIVE,
|
||||||
or state.has_all((item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player)
|
item_names.MUTALISK_VICIOUS_GLAIVE,
|
||||||
)
|
), self.player) >= 2
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
# Heal
|
# Heal
|
||||||
(
|
(
|
||||||
state.has(item_names.SWARM_QUEEN, self.player)
|
state.has(item_names.SWARM_QUEEN, self.player)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and ((self.morph_tyrannozor(state) and state.has(item_names.TYRANNOZOR_HEALING_ADAPTATION, self.player)) or (usable_muta))
|
and (
|
||||||
|
(
|
||||||
|
self.morph_tyrannozor(state)
|
||||||
|
and state.has(item_names.TYRANNOZOR_HEALING_ADAPTATION, self.player)
|
||||||
|
)
|
||||||
|
or usable_muta
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# Cross the gap
|
# Cross the gap
|
||||||
and (
|
and (
|
||||||
@@ -2147,9 +2179,10 @@ class SC2Logic:
|
|||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
state.has_any((item_names.WRAITH, item_names.BATTLECRUISER), self.player)
|
state.has_any((item_names.WRAITH, item_names.BATTLECRUISER), self.player)
|
||||||
or self.terran_air_anti_air(state)
|
or (self.terran_air_anti_air(state)
|
||||||
and state.has_any((item_names.BANSHEE, item_names.LIBERATOR), self.player)
|
and state.has_any((item_names.BANSHEE, item_names.LIBERATOR), self.player)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> bool:
|
def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> bool:
|
||||||
power_rating = self.zerg_power_rating(state)
|
power_rating = self.zerg_power_rating(state)
|
||||||
@@ -2488,8 +2521,7 @@ class SC2Logic:
|
|||||||
self.grant_story_tech == GrantStoryTech.option_grant
|
self.grant_story_tech == GrantStoryTech.option_grant
|
||||||
or self.advanced_tactics
|
or self.advanced_tactics
|
||||||
or (
|
or (
|
||||||
state.has_any(
|
state.has_any((
|
||||||
(
|
|
||||||
item_names.IMMORTAL,
|
item_names.IMMORTAL,
|
||||||
item_names.ANNIHILATOR,
|
item_names.ANNIHILATOR,
|
||||||
item_names.VANGUARD,
|
item_names.VANGUARD,
|
||||||
@@ -2500,17 +2532,18 @@ class SC2Logic:
|
|||||||
item_names.HIGH_TEMPLAR,
|
item_names.HIGH_TEMPLAR,
|
||||||
item_names.ENERGIZER,
|
item_names.ENERGIZER,
|
||||||
item_names.SENTRY,
|
item_names.SENTRY,
|
||||||
),
|
), self.player)
|
||||||
self.player,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def templars_return_phase_3_reach_colossus_requirement(self, state: CollectionState) -> bool:
|
def templars_return_phase_3_reach_colossus_requirement(self, state: CollectionState) -> bool:
|
||||||
return self.templars_return_phase_2_requirement(state) and (
|
return self.templars_return_phase_2_requirement(state) and (
|
||||||
self.grant_story_tech == GrantStoryTech.option_grant
|
self.grant_story_tech == GrantStoryTech.option_grant
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and state.has_any({item_names.ZEALOT_WHIRLWIND, item_names.VANGUARD_RAPIDFIRE_CANNON}, self.player)
|
and state.has_any((
|
||||||
|
item_names.ZEALOT_WHIRLWIND, item_names.VANGUARD_RAPIDFIRE_CANNON,
|
||||||
|
), self.player)
|
||||||
|
)
|
||||||
or state.has_all((
|
or state.has_all((
|
||||||
item_names.ZEALOT_WHIRLWIND, item_names.VANGUARD_RAPIDFIRE_CANNON
|
item_names.ZEALOT_WHIRLWIND, item_names.VANGUARD_RAPIDFIRE_CANNON
|
||||||
), self.player)
|
), self.player)
|
||||||
@@ -2519,19 +2552,16 @@ class SC2Logic:
|
|||||||
def templars_return_phase_3_reach_dts_requirement(self, state: CollectionState) -> bool:
|
def templars_return_phase_3_reach_dts_requirement(self, state: CollectionState) -> bool:
|
||||||
return self.templars_return_phase_3_reach_colossus_requirement(state) and (
|
return self.templars_return_phase_3_reach_colossus_requirement(state) and (
|
||||||
self.grant_story_tech == GrantStoryTech.option_grant
|
self.grant_story_tech == GrantStoryTech.option_grant
|
||||||
or (
|
or state.has_all((
|
||||||
(self.advanced_tactics or state.has(item_names.ENERGIZER_MOBILE_CHRONO_BEAM, self.player))
|
|
||||||
and (state.has(item_names.COLOSSUS_FIRE_LANCE, self.player)
|
|
||||||
or (
|
|
||||||
state.has_all(
|
|
||||||
{
|
|
||||||
item_names.COLOSSUS_PACIFICATION_PROTOCOL,
|
item_names.COLOSSUS_PACIFICATION_PROTOCOL,
|
||||||
item_names.ENERGIZER_MOBILE_CHRONO_BEAM,
|
item_names.ENERGIZER_MOBILE_CHRONO_BEAM,
|
||||||
},
|
), self.player)
|
||||||
self.player,
|
or (
|
||||||
|
state.has(item_names.COLOSSUS_FIRE_LANCE, self.player)
|
||||||
|
and (self.advanced_tactics
|
||||||
|
or state.has(item_names.ENERGIZER_MOBILE_CHRONO_BEAM, self.player)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def terran_spear_of_adun_requirement(self, state: CollectionState) -> bool:
|
def terran_spear_of_adun_requirement(self, state: CollectionState) -> bool:
|
||||||
@@ -2627,8 +2657,9 @@ class SC2Logic:
|
|||||||
self.morph_lurker(state)
|
self.morph_lurker(state)
|
||||||
or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE}, self.player)
|
or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE}, self.player)
|
||||||
or self.zerg_infested_tank_with_ammo(state)
|
or self.zerg_infested_tank_with_ammo(state)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and state.has_all({item_names.ULTRALISK, item_names.ULTRALISK_CHITINOUS_PLATING, item_names.ULTRALISK_MONARCH_BLADES}, self.player)
|
and state.has_all((item_names.ULTRALISK, item_names.ULTRALISK_CHITINOUS_PLATING, item_names.ULTRALISK_MONARCH_BLADES), self.player)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
and (
|
and (
|
||||||
self.morph_impaler(state)
|
self.morph_impaler(state)
|
||||||
@@ -2639,9 +2670,10 @@ class SC2Logic:
|
|||||||
and (
|
and (
|
||||||
self.morph_devourer(state)
|
self.morph_devourer(state)
|
||||||
or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player)
|
or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player)
|
||||||
or self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and state.has(item_names.BROOD_QUEEN, self.player)
|
and state.has(item_names.BROOD_QUEEN, self.player)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
and self.zerg_mineral_dump(state)
|
and self.zerg_mineral_dump(state)
|
||||||
and self.zerg_army_weapon_armor_upgrade_min_level(state) >= 2
|
and self.zerg_army_weapon_armor_upgrade_min_level(state) >= 2
|
||||||
)
|
)
|
||||||
@@ -2674,9 +2706,10 @@ class SC2Logic:
|
|||||||
return (
|
return (
|
||||||
self.protoss_anti_armor_anti_air(state)
|
self.protoss_anti_armor_anti_air(state)
|
||||||
and (
|
and (
|
||||||
self.take_over_ai_allies
|
(self.protoss_competent_comp(state) and self.protoss_hybrid_counter(state))
|
||||||
|
or (self.take_over_ai_allies
|
||||||
and (self.protoss_common_unit(state) or self.zerg_common_unit(state))
|
and (self.protoss_common_unit(state) or self.zerg_common_unit(state))
|
||||||
or (self.protoss_competent_comp(state) and self.protoss_hybrid_counter(state))
|
)
|
||||||
)
|
)
|
||||||
and self.protoss_power_rating(state) >= 6
|
and self.protoss_power_rating(state) >= 6
|
||||||
)
|
)
|
||||||
@@ -2685,11 +2718,18 @@ class SC2Logic:
|
|||||||
return (
|
return (
|
||||||
self.terran_competent_anti_air(state)
|
self.terran_competent_anti_air(state)
|
||||||
and (
|
and (
|
||||||
|
(
|
||||||
|
self.terran_beats_protoss_deathball(state)
|
||||||
|
and state.has_any((
|
||||||
|
item_names.BATTLECRUISER,
|
||||||
|
item_names.LIBERATOR,
|
||||||
|
item_names.SIEGE_TANK,
|
||||||
|
item_names.THOR,
|
||||||
|
), self.player)
|
||||||
|
)
|
||||||
|
or (
|
||||||
self.take_over_ai_allies
|
self.take_over_ai_allies
|
||||||
and (self.terran_common_unit(state) or self.zerg_common_unit(state))
|
and (self.terran_common_unit(state) or self.zerg_common_unit(state))
|
||||||
or (
|
|
||||||
self.terran_beats_protoss_deathball(state)
|
|
||||||
and state.has_any({item_names.BATTLECRUISER, item_names.LIBERATOR, item_names.SIEGE_TANK, item_names.THOR}, self.player)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
and self.terran_power_rating(state) >= 6
|
and self.terran_power_rating(state) >= 6
|
||||||
@@ -2727,10 +2767,12 @@ class SC2Logic:
|
|||||||
state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_SMART_SERVOS}, self.player)
|
state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_SMART_SERVOS}, self.player)
|
||||||
and (
|
and (
|
||||||
(
|
(
|
||||||
state.has_all({item_names.HELLION, item_names.HELLION_HELLBAT}, self.player)
|
(
|
||||||
|
state.has_all((item_names.HELLION, item_names.HELLION_HELLBAT), self.player)
|
||||||
or state.has(item_names.FIREBAT, self.player)
|
or state.has(item_names.FIREBAT, self.player)
|
||||||
)
|
)
|
||||||
and self.terran_bio_heal(state)
|
and self.terran_bio_heal(state)
|
||||||
|
)
|
||||||
or state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player)
|
or state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player)
|
||||||
or state.has(item_names.BANSHEE, self.player)
|
or state.has(item_names.BANSHEE, self.player)
|
||||||
)
|
)
|
||||||
@@ -2921,13 +2963,21 @@ class SC2Logic:
|
|||||||
|
|
||||||
def protoss_the_host_requirement(self, state: CollectionState) -> bool:
|
def protoss_the_host_requirement(self, state: CollectionState) -> bool:
|
||||||
return (
|
return (
|
||||||
self.protoss_fleet(state) and self.protoss_static_defense(state) and self.protoss_army_weapon_armor_upgrade_min_level(state) >= 2
|
(
|
||||||
) or (
|
self.protoss_fleet(state)
|
||||||
|
and self.protoss_static_defense(state)
|
||||||
|
and self.protoss_army_weapon_armor_upgrade_min_level(state) >= 2
|
||||||
|
)
|
||||||
|
or (
|
||||||
self.protoss_deathball(state)
|
self.protoss_deathball(state)
|
||||||
and state.has(item_names.SOA_TIME_STOP, self.player)
|
and (
|
||||||
or self.advanced_tactics
|
state.has(item_names.SOA_TIME_STOP, self.player)
|
||||||
|
or (self.advanced_tactics
|
||||||
and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT), self.player))
|
and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT), self.player))
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def terran_the_host_requirement(self, state: CollectionState) -> bool:
|
def terran_the_host_requirement(self, state: CollectionState) -> bool:
|
||||||
return (
|
return (
|
||||||
@@ -2947,10 +2997,21 @@ class SC2Logic:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
self.spear_of_adun_presence in (SpearOfAdunPresence.option_everywhere, SpearOfAdunPresence.option_any_race_lotv)
|
(
|
||||||
and state.has(item_names.SOA_TIME_STOP, self.player)
|
self.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere
|
||||||
or self.advanced_tactics
|
or self.spear_of_adun_presence == SpearOfAdunPresence.option_any_race_lotv
|
||||||
and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT), self.player))
|
)
|
||||||
|
and (
|
||||||
|
state.has(item_names.SOA_TIME_STOP, self.player)
|
||||||
|
or (self.advanced_tactics
|
||||||
|
and (
|
||||||
|
state.has_any((
|
||||||
|
item_names.SOA_SHIELD_OVERCHARGE,
|
||||||
|
item_names.SOA_SOLAR_BOMBARDMENT,
|
||||||
|
), self.player)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -2991,12 +3052,19 @@ class SC2Logic:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
self.spear_of_adun_presence in (SpearOfAdunPresence.option_everywhere, SpearOfAdunPresence.option_any_race_lotv)
|
(
|
||||||
|
self.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere
|
||||||
|
or self.spear_of_adun_presence == SpearOfAdunPresence.option_any_race_lotv
|
||||||
|
)
|
||||||
and (
|
and (
|
||||||
state.has(item_names.SOA_TIME_STOP, self.player)
|
state.has(item_names.SOA_TIME_STOP, self.player)
|
||||||
or (self.advanced_tactics
|
or (self.advanced_tactics
|
||||||
and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT),
|
and (
|
||||||
self.player))
|
state.has_any((
|
||||||
|
item_names.SOA_SHIELD_OVERCHARGE,
|
||||||
|
item_names.SOA_SOLAR_BOMBARDMENT
|
||||||
|
), self.player)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -3263,23 +3331,26 @@ class SC2Logic:
|
|||||||
(
|
(
|
||||||
# Regular infesteds
|
# Regular infesteds
|
||||||
(
|
(
|
||||||
|
self.terran_bio_heal(state)
|
||||||
|
and (
|
||||||
state.has_any((item_names.FIREBAT, item_names.REAPER), self.player)
|
state.has_any((item_names.FIREBAT, item_names.REAPER), self.player)
|
||||||
or state.has_all({item_names.HELLION, item_names.HELLION_HELLBAT}, self.player)
|
or state.has_all((item_names.HELLION, item_names.HELLION_HELLBAT), self.player)
|
||||||
)
|
)
|
||||||
and self.terran_bio_heal(state)
|
)
|
||||||
or (self.advanced_tactics and state.has_any({item_names.PERDITION_TURRET, item_names.PLANETARY_FORTRESS}, self.player))
|
or (self.advanced_tactics and state.has_any((item_names.PERDITION_TURRET, item_names.PLANETARY_FORTRESS), self.player))
|
||||||
)
|
)
|
||||||
and (
|
and (
|
||||||
# Volatile infesteds
|
# Volatile infesteds
|
||||||
state.has(item_names.LIBERATOR, self.player)
|
state.has(item_names.LIBERATOR, self.player)
|
||||||
or (
|
or (
|
||||||
self.advanced_tactics
|
self.advanced_tactics
|
||||||
and state.has(item_names.VULTURE, self.player)
|
and (state.has(item_names.VULTURE, self.player)
|
||||||
or (state.has(item_names.HERC, self.player) and self.terran_bio_heal(state))
|
or (state.has(item_names.HERC, self.player) and self.terran_bio_heal(state))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2
|
and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3317,14 +3388,25 @@ class SC2Logic:
|
|||||||
def enemy_shadow_first_stage(self, state: CollectionState) -> bool:
|
def enemy_shadow_first_stage(self, state: CollectionState) -> bool:
|
||||||
return self.enemy_shadow_domination(state) and (
|
return self.enemy_shadow_domination(state) and (
|
||||||
self.grant_story_tech == GrantStoryTech.option_grant
|
self.grant_story_tech == GrantStoryTech.option_grant
|
||||||
or ((self.nova_full_stealth(state) and self.enemy_shadow_tripwires_tool(state)) or (self.nova_heal(state) and self.nova_splash(state)))
|
or (self.nova_full_stealth(state)
|
||||||
|
and self.enemy_shadow_tripwires_tool(state)
|
||||||
|
)
|
||||||
|
or (self.nova_heal(state)
|
||||||
|
and self.nova_splash(state)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def enemy_shadow_second_stage(self, state: CollectionState) -> bool:
|
def enemy_shadow_second_stage(self, state: CollectionState) -> bool:
|
||||||
return self.enemy_shadow_first_stage(state) and (
|
return self.enemy_shadow_first_stage(state) and (
|
||||||
self.grant_story_tech == GrantStoryTech.option_grant
|
self.grant_story_tech == GrantStoryTech.option_grant
|
||||||
or (self.nova_splash(state) or self.nova_heal(state) or self.nova_escape_assist(state))
|
or (
|
||||||
and (self.advanced_tactics or state.has(item_names.NOVA_GHOST_VISOR, self.player))
|
(self.advanced_tactics or state.has(item_names.NOVA_GHOST_VISOR, self.player))
|
||||||
|
and (
|
||||||
|
self.nova_splash(state)
|
||||||
|
or self.nova_heal(state)
|
||||||
|
or self.nova_escape_assist(state)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def enemy_shadow_door_controls(self, state: CollectionState) -> bool:
|
def enemy_shadow_door_controls(self, state: CollectionState) -> bool:
|
||||||
@@ -3617,3 +3699,124 @@ def get_basic_units(logic_level: int, race: SC2Race) -> Set[str]:
|
|||||||
return advanced_basic_units[race]
|
return advanced_basic_units[race]
|
||||||
else:
|
else:
|
||||||
return basic_units[race]
|
return basic_units[race]
|
||||||
|
|
||||||
|
|
||||||
|
# Defense rating table
|
||||||
|
# Commented defense ratings are handled in the defense_rating function
|
||||||
|
tvx_defense_ratings = {
|
||||||
|
item_names.SIEGE_TANK: 5,
|
||||||
|
# "Graduating Range": 1,
|
||||||
|
item_names.PLANETARY_FORTRESS: 3,
|
||||||
|
# Bunker w/ Marine/Marauder: 3,
|
||||||
|
item_names.PERDITION_TURRET: 2,
|
||||||
|
item_names.DEVASTATOR_TURRET: 2,
|
||||||
|
item_names.VULTURE: 1,
|
||||||
|
item_names.BANSHEE: 1,
|
||||||
|
item_names.BATTLECRUISER: 1,
|
||||||
|
item_names.LIBERATOR: 4,
|
||||||
|
item_names.WIDOW_MINE: 1,
|
||||||
|
# "Concealment (Widow Mine)": 1
|
||||||
|
}
|
||||||
|
tvz_defense_ratings = {
|
||||||
|
item_names.PERDITION_TURRET: 2,
|
||||||
|
# Bunker w/ Firebat: 2,
|
||||||
|
item_names.LIBERATOR: -2,
|
||||||
|
item_names.HIVE_MIND_EMULATOR: 3,
|
||||||
|
item_names.PSI_DISRUPTER: 3,
|
||||||
|
}
|
||||||
|
tvx_air_defense_ratings = {
|
||||||
|
item_names.MISSILE_TURRET: 2,
|
||||||
|
}
|
||||||
|
zvx_defense_ratings = {
|
||||||
|
# Note that this doesn't include Kerrigan because this is just for race swaps, which doesn't involve her (for now)
|
||||||
|
item_names.SPINE_CRAWLER: 3,
|
||||||
|
# w/ Twin Drones: 1
|
||||||
|
item_names.SWARM_QUEEN: 1,
|
||||||
|
item_names.SWARM_HOST: 1,
|
||||||
|
# impaler: 3
|
||||||
|
# "Hardened Tentacle Spines (Impaler)": 2
|
||||||
|
# lurker: 1
|
||||||
|
# "Seismic Spines (Lurker)": 2
|
||||||
|
# "Adapted Spines (Lurker)": 1
|
||||||
|
# brood lord : 2
|
||||||
|
# corpser roach: 1
|
||||||
|
# creep tumors (swarm queen or overseer): 1
|
||||||
|
# w/ malignant creep: 1
|
||||||
|
# tanks with ammo: 5
|
||||||
|
item_names.INFESTED_BUNKER: 3,
|
||||||
|
item_names.BILE_LAUNCHER: 2,
|
||||||
|
}
|
||||||
|
# zvz_defense_ratings = {
|
||||||
|
# corpser roach: 1
|
||||||
|
# primal igniter: 2
|
||||||
|
# lurker: 1
|
||||||
|
# w/ adapted spines: -1
|
||||||
|
# impaler: -1
|
||||||
|
# }
|
||||||
|
zvx_air_defense_ratings = {
|
||||||
|
item_names.SPORE_CRAWLER: 2,
|
||||||
|
# w/ Twin Drones: 1
|
||||||
|
item_names.INFESTED_MISSILE_TURRET: 2,
|
||||||
|
}
|
||||||
|
pvx_defense_ratings = {
|
||||||
|
item_names.PHOTON_CANNON: 2,
|
||||||
|
item_names.KHAYDARIN_MONOLITH: 3,
|
||||||
|
item_names.SHIELD_BATTERY: 1,
|
||||||
|
item_names.NEXUS_OVERCHARGE: 2,
|
||||||
|
item_names.SKYLORD: 1,
|
||||||
|
item_names.MATRIX_OVERLOAD: 1,
|
||||||
|
item_names.COLOSSUS: 1,
|
||||||
|
item_names.VANGUARD: 1,
|
||||||
|
item_names.REAVER: 1,
|
||||||
|
}
|
||||||
|
pvz_defense_ratings = {
|
||||||
|
item_names.KHAYDARIN_MONOLITH: -2,
|
||||||
|
item_names.COLOSSUS: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
terran_passive_ratings = {
|
||||||
|
item_names.AUTOMATED_REFINERY: 4,
|
||||||
|
item_names.COMMAND_CENTER_MULE: 4,
|
||||||
|
item_names.ORBITAL_DEPOTS: 2,
|
||||||
|
item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: 2,
|
||||||
|
item_names.COMMAND_CENTER_EXTRA_SUPPLIES: 2,
|
||||||
|
item_names.MICRO_FILTERING: 2,
|
||||||
|
item_names.TECH_REACTOR: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
zerg_passive_ratings = {
|
||||||
|
item_names.TWIN_DRONES: 7,
|
||||||
|
item_names.AUTOMATED_EXTRACTORS: 4,
|
||||||
|
item_names.VESPENE_EFFICIENCY: 3,
|
||||||
|
item_names.OVERLORD_IMPROVED_OVERLORDS: 4,
|
||||||
|
item_names.MALIGNANT_CREEP: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
protoss_passive_ratings = {
|
||||||
|
item_names.QUATRO: 4,
|
||||||
|
item_names.ORBITAL_ASSIMILATORS: 4,
|
||||||
|
item_names.AMPLIFIED_ASSIMILATORS: 3,
|
||||||
|
item_names.PROBE_WARPIN: 2,
|
||||||
|
item_names.ELDER_PROBES: 2,
|
||||||
|
item_names.MATRIX_OVERLOAD: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
soa_energy_ratings = {
|
||||||
|
item_names.SOA_SOLAR_LANCE: 8,
|
||||||
|
item_names.SOA_DEPLOY_FENIX: 7,
|
||||||
|
item_names.SOA_TEMPORAL_FIELD: 6,
|
||||||
|
item_names.SOA_PROGRESSIVE_PROXY_PYLON: 5, # Requires Lvl 2 (Warp in Reinforcements)
|
||||||
|
item_names.SOA_SHIELD_OVERCHARGE: 5,
|
||||||
|
item_names.SOA_ORBITAL_STRIKE: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
soa_passive_ratings = {
|
||||||
|
item_names.GUARDIAN_SHELL: 4,
|
||||||
|
item_names.OVERWATCH: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
soa_ultimate_ratings = {
|
||||||
|
item_names.SOA_TIME_STOP: 4,
|
||||||
|
item_names.SOA_PURIFIER_BEAM: 3,
|
||||||
|
item_names.SOA_SOLAR_BOMBARDMENT: 3
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
world_options = {
|
world_options = {
|
||||||
**self.ALL_CAMPAIGNS,
|
**self.ALL_CAMPAIGNS,
|
||||||
'locked_items': {
|
'locked_items': {
|
||||||
item_names.MARINE: 0,
|
item_names.MARINE: -1,
|
||||||
item_names.MARAUDER: 0,
|
item_names.MARAUDER: -1,
|
||||||
item_names.MEDIVAC: 1,
|
item_names.MEDIVAC: 1,
|
||||||
item_names.FIREBAT: 1,
|
item_names.FIREBAT: 1,
|
||||||
item_names.ZEALOT: 0,
|
item_names.ZEALOT: -1,
|
||||||
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
|
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
|
||||||
},
|
},
|
||||||
'excluded_items': {
|
'excluded_items': {
|
||||||
item_names.MARINE: 0,
|
item_names.MARINE: -1,
|
||||||
item_names.MARAUDER: 0,
|
item_names.MARAUDER: -1,
|
||||||
item_names.MEDIVAC: 0,
|
item_names.MEDIVAC: -1,
|
||||||
item_names.FIREBAT: 1,
|
item_names.FIREBAT: 1,
|
||||||
item_names.ZERGLING: 0,
|
item_names.ZERGLING: -1,
|
||||||
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
|
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,38 +50,38 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
world_options = {
|
world_options = {
|
||||||
'grant_story_tech': options.GrantStoryTech.option_grant,
|
'grant_story_tech': options.GrantStoryTech.option_grant,
|
||||||
'excluded_items': {
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.NOVA_EQUIPMENT: 15,
|
item_groups.ItemGroupNames.NOVA_EQUIPMENT: -1,
|
||||||
item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
|
item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
|
||||||
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2,
|
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2,
|
||||||
item_names.MARINE: 0,
|
item_names.MARINE: -1,
|
||||||
item_names.MARAUDER: 0,
|
item_names.MARAUDER: -1,
|
||||||
item_names.REAPER: 1,
|
item_names.REAPER: 1,
|
||||||
item_names.DIAMONDBACK: 0,
|
item_names.DIAMONDBACK: -1,
|
||||||
item_names.HELLION: 1,
|
item_names.HELLION: 1,
|
||||||
# Additional excludes to increase the likelihood that unexcluded items actually appear
|
# Additional excludes to increase the likelihood that unexcluded items actually appear
|
||||||
item_groups.ItemGroupNames.STARPORT_UNITS: 0,
|
item_groups.ItemGroupNames.STARPORT_UNITS: -1,
|
||||||
item_names.WARHOUND: 0,
|
item_names.WARHOUND: -1,
|
||||||
item_names.VULTURE: 0,
|
item_names.VULTURE: -1,
|
||||||
item_names.WIDOW_MINE: 0,
|
item_names.WIDOW_MINE: -1,
|
||||||
item_names.THOR: 0,
|
item_names.THOR: -1,
|
||||||
item_names.GHOST: 0,
|
item_names.GHOST: -1,
|
||||||
item_names.SPECTRE: 0,
|
item_names.SPECTRE: -1,
|
||||||
item_groups.ItemGroupNames.MENGSK_UNITS: 0,
|
item_groups.ItemGroupNames.MENGSK_UNITS: -1,
|
||||||
item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0,
|
item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: -1,
|
||||||
},
|
},
|
||||||
'unexcluded_items': {
|
'unexcluded_items': {
|
||||||
item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic
|
item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic
|
||||||
item_names.NOVA_PULSE_GRENADES: 0, # Necessary to pass logic
|
item_names.NOVA_PULSE_GRENADES: -1, # Necessary to pass logic
|
||||||
item_names.NOVA_JUMP_SUIT_MODULE: 0, # Necessary to pass logic
|
item_names.NOVA_JUMP_SUIT_MODULE: -1, # Necessary to pass logic
|
||||||
item_groups.ItemGroupNames.BARRACKS_UNITS: 0,
|
item_groups.ItemGroupNames.BARRACKS_UNITS: -1,
|
||||||
item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1,
|
item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1,
|
||||||
item_names.HELLION: 1,
|
item_names.HELLION: 1,
|
||||||
item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
|
item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
|
||||||
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 0,
|
item_names.MARAUDER_PROGRESSIVE_STIMPACK: -1,
|
||||||
# Additional unexcludes for logic
|
# Additional unexcludes for logic
|
||||||
item_names.MEDIVAC: 0,
|
item_names.MEDIVAC: -1,
|
||||||
item_names.BATTLECRUISER: 0,
|
item_names.BATTLECRUISER: -1,
|
||||||
item_names.SCIENCE_VESSEL: 0,
|
item_names.SCIENCE_VESSEL: -1,
|
||||||
},
|
},
|
||||||
# Terran-only
|
# Terran-only
|
||||||
'enabled_campaigns': {
|
'enabled_campaigns': {
|
||||||
@@ -103,11 +103,29 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool)
|
self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool)
|
||||||
self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool)
|
self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool)
|
||||||
|
|
||||||
|
def test_exclude_2_beats_unexclude_1(self) -> None:
|
||||||
|
world_options = {
|
||||||
|
options.OPTION_NAME[options.ExcludedItems]: {
|
||||||
|
item_names.MARINE: 2,
|
||||||
|
},
|
||||||
|
options.OPTION_NAME[options.UnexcludedItems]: {
|
||||||
|
item_names.MARINE: 1,
|
||||||
|
},
|
||||||
|
# Ensure enough locations that marine doesn't get culled
|
||||||
|
options.OPTION_NAME[options.SelectedRaces]: {
|
||||||
|
SC2Race.TERRAN.get_title(),
|
||||||
|
},
|
||||||
|
options.OPTION_NAME[options.VictoryCache]: 9,
|
||||||
|
}
|
||||||
|
self.generate_world(world_options)
|
||||||
|
itempool = [item.name for item in self.multiworld.itempool]
|
||||||
|
self.assertNotIn(item_names.MARINE, itempool)
|
||||||
|
|
||||||
def test_excluding_groups_excludes_all_items_in_group(self):
|
def test_excluding_groups_excludes_all_items_in_group(self):
|
||||||
world_options = {
|
world_options = {
|
||||||
'excluded_items': [
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(),
|
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(): -1,
|
||||||
]
|
},
|
||||||
}
|
}
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
itempool = [item.name for item in self.multiworld.itempool]
|
itempool = [item.name for item in self.multiworld.itempool]
|
||||||
@@ -337,9 +355,9 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
# Options under test
|
# Options under test
|
||||||
'vanilla_items_only': True,
|
'vanilla_items_only': True,
|
||||||
'unexcluded_items': {
|
'unexcluded_items': {
|
||||||
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: 0,
|
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: -1,
|
||||||
item_names.WARHOUND: 1,
|
item_names.WARHOUND: 1,
|
||||||
item_groups.ItemGroupNames.TERRAN_STIMPACKS: 0,
|
item_groups.ItemGroupNames.TERRAN_STIMPACKS: -1,
|
||||||
},
|
},
|
||||||
# Avoid options that lock non-vanilla items for logic
|
# Avoid options that lock non-vanilla items for logic
|
||||||
'required_tactics': options.RequiredTactics.option_any_units,
|
'required_tactics': options.RequiredTactics.option_any_units,
|
||||||
@@ -463,12 +481,12 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
},
|
},
|
||||||
'required_tactics': options.RequiredTactics.option_no_logic,
|
'required_tactics': options.RequiredTactics.option_no_logic,
|
||||||
'enable_morphling': options.EnableMorphling.option_true,
|
'enable_morphling': options.EnableMorphling.option_true,
|
||||||
'excluded_items': [
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.ZERG_UNITS.lower()
|
item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1,
|
||||||
],
|
},
|
||||||
'unexcluded_items': [
|
'unexcluded_items': {
|
||||||
item_groups.ItemGroupNames.ZERG_MORPHS.lower()
|
item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1,
|
||||||
]
|
},
|
||||||
}
|
}
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
itempool = [item.name for item in self.multiworld.itempool]
|
itempool = [item.name for item in self.multiworld.itempool]
|
||||||
@@ -486,12 +504,12 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
},
|
},
|
||||||
'required_tactics': options.RequiredTactics.option_no_logic,
|
'required_tactics': options.RequiredTactics.option_no_logic,
|
||||||
'enable_morphling': options.EnableMorphling.option_false,
|
'enable_morphling': options.EnableMorphling.option_false,
|
||||||
'excluded_items': [
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.ZERG_UNITS.lower()
|
item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1,
|
||||||
],
|
},
|
||||||
'unexcluded_items': [
|
'unexcluded_items': {
|
||||||
item_groups.ItemGroupNames.ZERG_MORPHS.lower()
|
item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1,
|
||||||
]
|
},
|
||||||
}
|
}
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
itempool = [item.name for item in self.multiworld.itempool]
|
itempool = [item.name for item in self.multiworld.itempool]
|
||||||
@@ -520,14 +538,14 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
|
|
||||||
def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None:
|
def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None:
|
||||||
world_options = {
|
world_options = {
|
||||||
"excluded_items": [
|
"excluded_items": {
|
||||||
item_names.COMMAND_CENTER_MULE,
|
item_names.COMMAND_CENTER_MULE: -1,
|
||||||
item_names.COMMAND_CENTER_SCANNER_SWEEP,
|
item_names.COMMAND_CENTER_SCANNER_SWEEP: -1,
|
||||||
item_names.COMMAND_CENTER_EXTRA_SUPPLIES
|
item_names.COMMAND_CENTER_EXTRA_SUPPLIES: -1,
|
||||||
],
|
},
|
||||||
"locked_items": [
|
"locked_items": {
|
||||||
item_names.PLANETARY_FORTRESS
|
item_names.PLANETARY_FORTRESS: -1,
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
@@ -931,10 +949,10 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'grant_story_levels': options.GrantStoryLevels.option_additive,
|
'grant_story_levels': options.GrantStoryLevels.option_additive,
|
||||||
'excluded_items': [
|
'excluded_items': {
|
||||||
item_names.KERRIGAN_LEAPING_STRIKE,
|
item_names.KERRIGAN_LEAPING_STRIKE: -1,
|
||||||
item_names.KERRIGAN_MEND,
|
item_names.KERRIGAN_MEND: -1,
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
itempool = [item.name for item in self.multiworld.itempool]
|
itempool = [item.name for item in self.multiworld.itempool]
|
||||||
@@ -1208,7 +1226,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
'mission_order': MissionOrder.option_grid,
|
'mission_order': MissionOrder.option_grid,
|
||||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||||
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
|
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
|
||||||
'locked_items': [locked_item],
|
'locked_items': {locked_item: -1},
|
||||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||||
'selected_races': [SC2Race.TERRAN.get_title()],
|
'selected_races': [SC2Race.TERRAN.get_title()],
|
||||||
}
|
}
|
||||||
@@ -1249,7 +1267,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||||
'exclude_overpowered_items': ExcludeOverpoweredItems.option_false,
|
'exclude_overpowered_items': ExcludeOverpoweredItems.option_false,
|
||||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||||
'locked_items': {item_name: 0 for item_name in unreleased_items},
|
'locked_items': {item_name: -1 for item_name in unreleased_items},
|
||||||
}
|
}
|
||||||
|
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
@@ -1264,7 +1282,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
**self.ALL_CAMPAIGNS,
|
**self.ALL_CAMPAIGNS,
|
||||||
'mission_order': MissionOrder.option_grid,
|
'mission_order': MissionOrder.option_grid,
|
||||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||||
'excluded_items': [item_name for item_name in item_groups.terran_mercenaries],
|
'excluded_items': {item_name: -1 for item_name in item_groups.terran_mercenaries},
|
||||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||||
'selected_races': [SC2Race.TERRAN.get_title()],
|
'selected_races': [SC2Race.TERRAN.get_title()],
|
||||||
}
|
}
|
||||||
@@ -1280,7 +1298,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
'mission_order': MissionOrder.option_grid,
|
'mission_order': MissionOrder.option_grid,
|
||||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||||
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
|
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
|
||||||
'unexcluded_items': [item_names.SOA_TIME_STOP],
|
'unexcluded_items': {item_names.SOA_TIME_STOP: -1},
|
||||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1322,7 +1340,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
|||||||
'enabled_campaigns': {
|
'enabled_campaigns': {
|
||||||
SC2Campaign.WOL.campaign_name
|
SC2Campaign.WOL.campaign_name
|
||||||
},
|
},
|
||||||
'excluded_items': [item_names.MARINE, item_names.MEDIC],
|
'excluded_items': {item_names.MARINE: -1, item_names.MEDIC: -1},
|
||||||
'shuffle_no_build': False,
|
'shuffle_no_build': False,
|
||||||
'required_tactics': RequiredTactics.option_standard
|
'required_tactics': RequiredTactics.option_standard
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ItemFilterTests(Sc2SetupTestBase):
|
|||||||
def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None:
|
def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None:
|
||||||
world_options = {
|
world_options = {
|
||||||
'excluded_items': {
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.BARRACKS_UNITS: 0
|
item_groups.ItemGroupNames.BARRACKS_UNITS: -1,
|
||||||
},
|
},
|
||||||
'required_tactics': 'standard',
|
'required_tactics': 'standard',
|
||||||
'min_number_of_upgrades': 1,
|
'min_number_of_upgrades': 1,
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
|||||||
SC2Campaign.NCO.campaign_name
|
SC2Campaign.NCO.campaign_name
|
||||||
},
|
},
|
||||||
'excluded_items': {
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
|
item_groups.ItemGroupNames.TERRAN_UNITS: -1,
|
||||||
},
|
},
|
||||||
'unexcluded_items': {
|
'unexcluded_items': {
|
||||||
item_groups.ItemGroupNames.NCO_UNITS: 0,
|
item_groups.ItemGroupNames.NCO_UNITS: -1,
|
||||||
},
|
},
|
||||||
'max_number_of_upgrades': 2,
|
'max_number_of_upgrades': 2,
|
||||||
}
|
}
|
||||||
@@ -81,10 +81,10 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
|||||||
},
|
},
|
||||||
'mission_order': options.MissionOrder.option_vanilla_shuffled,
|
'mission_order': options.MissionOrder.option_vanilla_shuffled,
|
||||||
'excluded_items': {
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.TERRAN_ITEMS: 0,
|
item_groups.ItemGroupNames.TERRAN_ITEMS: -1,
|
||||||
},
|
},
|
||||||
'unexcluded_items': {
|
'unexcluded_items': {
|
||||||
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0,
|
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: -1,
|
||||||
item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1,
|
item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1,
|
||||||
},
|
},
|
||||||
'excluded_missions': [
|
'excluded_missions': [
|
||||||
@@ -398,7 +398,7 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
|||||||
|
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
world_item_names = [item.name for item in self.multiworld.itempool]
|
world_item_names = [item.name for item in self.multiworld.itempool]
|
||||||
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns]
|
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_actives]
|
||||||
|
|
||||||
self.assertLessEqual(len(spear_of_adun_actives), target_number)
|
self.assertLessEqual(len(spear_of_adun_actives), target_number)
|
||||||
|
|
||||||
@@ -418,7 +418,9 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
|||||||
|
|
||||||
self.generate_world(world_options)
|
self.generate_world(world_options)
|
||||||
world_item_names = [item.name for item in self.multiworld.itempool]
|
world_item_names = [item.name for item in self.multiworld.itempool]
|
||||||
spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives]
|
spear_of_adun_autocasts = [
|
||||||
|
item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_passives
|
||||||
|
]
|
||||||
|
|
||||||
self.assertLessEqual(len(spear_of_adun_autocasts), target_number)
|
self.assertLessEqual(len(spear_of_adun_autocasts), target_number)
|
||||||
|
|
||||||
@@ -471,12 +473,12 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
|||||||
],
|
],
|
||||||
'required_tactics': options.RequiredTactics.option_any_units,
|
'required_tactics': options.RequiredTactics.option_any_units,
|
||||||
'excluded_items': {
|
'excluded_items': {
|
||||||
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
|
item_groups.ItemGroupNames.TERRAN_UNITS: -1,
|
||||||
item_groups.ItemGroupNames.ZERG_UNITS: 0,
|
item_groups.ItemGroupNames.ZERG_UNITS: -1,
|
||||||
},
|
},
|
||||||
'unexcluded_items': {
|
'unexcluded_items': {
|
||||||
item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0,
|
item_groups.ItemGroupNames.TERRAN_MERCENARIES: -1,
|
||||||
item_groups.ItemGroupNames.ZERG_MERCENARIES: 0,
|
item_groups.ItemGroupNames.ZERG_MERCENARIES: -1,
|
||||||
},
|
},
|
||||||
'start_inventory': {
|
'start_inventory': {
|
||||||
item_names.PROGRESSIVE_FAST_DELIVERY: 1,
|
item_names.PROGRESSIVE_FAST_DELIVERY: 1,
|
||||||
|
|||||||
@@ -306,8 +306,8 @@ class ShapezWorld(World):
|
|||||||
self.location_count = len(self.included_locations)
|
self.location_count = len(self.included_locations)
|
||||||
|
|
||||||
# Create regions and entrances based on included locations and player options
|
# Create regions and entrances based on included locations and player options
|
||||||
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld,
|
has_floating = self.options.allow_floating_layers.value or not (self.options.randomize_level_requirements and self.options.randomize_upgrade_requirements)
|
||||||
bool(self.options.allow_floating_layers.value),
|
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld, has_floating,
|
||||||
self.included_locations, self.location_name_to_id,
|
self.included_locations, self.location_name_to_id,
|
||||||
self.level_logic, self.upgrade_logic,
|
self.level_logic, self.upgrade_logic,
|
||||||
self.options.early_balancer_tunnel_and_trash.current_key,
|
self.options.early_balancer_tunnel_and_trash.current_key,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.co
|
|||||||
|
|
||||||
## Woher bekomme ich eine Konfigurationsdatei?
|
## Woher bekomme ich eine Konfigurationsdatei?
|
||||||
|
|
||||||
Die [Player Options](https://archipelago.gg/games/Timespinner/player-options) Seite auf der Website erlaubt dir,
|
Die [Player Options](/games/Timespinner/player-options) Seite auf der Website erlaubt dir,
|
||||||
persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
|
persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
|
||||||
|
|
||||||
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an
|
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an
|
||||||
|
|||||||
@@ -1047,7 +1047,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
|
|||||||
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
||||||
regions["Rooted Ziggurat Portal Room"].connect(
|
regions["Rooted Ziggurat Portal Room"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Portal"],
|
connecting_region=regions["Rooted Ziggurat Portal"],
|
||||||
rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world) and has_ability(prayer, state, world))
|
rule=lambda state: has_ability(prayer, state, world))
|
||||||
|
|
||||||
regions["Rooted Ziggurat Portal Room"].connect(
|
regions["Rooted Ziggurat Portal Room"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
||||||
|
|||||||
Reference in New Issue
Block a user