SC2: UI update, Relegate No Build Option, and Filler Item Update (#606)

This commit is contained in:
TheCondor07 2022-06-03 14:18:36 -04:00 committed by GitHub
parent f5dc39ddf0
commit 0dd67f40ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 58 deletions

View File

@ -36,7 +36,7 @@ nest_asyncio.apply()
class StarcraftClientProcessor(ClientCommandProcessor):
ctx: Context
ctx: SC2Context
def _cmd_disable_mission_check(self) -> bool:
"""Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
@ -74,7 +74,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
return True
class Context(CommonContext):
class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor
game = "Starcraft 2 Wings of Liberty"
items_handling = 0b111
@ -89,10 +89,12 @@ class Context(CommonContext):
announcement_pos = 0
sc2_run_task: typing.Optional[asyncio.Task] = None
missions_unlocked = False
current_tooltip = None
last_loc_list = None
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(Context, self).server_auth(password_requested)
await super(SC2Context, self).server_auth(password_requested)
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
@ -105,6 +107,10 @@ class Context(CommonContext):
self.all_in_choice = args["slot_data"]["all_in_map"]
slot_req_table = args["slot_data"]["mission_req"]
self.mission_req_table = {}
# Compatibility for 0.3.2 server data.
if "category" not in next(iter(slot_req_table)):
for i, mission_data in enumerate(slot_req_table.values()):
mission_data["category"] = wol_default_categories[i]
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
@ -119,19 +125,53 @@ class Context(CommonContext):
self.announcements.append(args["data"])
def run_gui(self):
from kvui import GameManager
from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import StringProperty
import Utils
class MissionButton(Button):
class HoverableButton(HoverBehavior, Button):
pass
class MissionButton(HoverableButton):
tooltip_text = StringProperty("Test")
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(*args, **kwargs)
self.layout = FloatLayout()
self.popuplabel = ServerToolTip(text=self.text)
self.layout.add_widget(self.popuplabel)
def on_enter(self):
self.popuplabel.text = self.tooltip_text
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
if self.tooltip_text == "":
self.ctx.current_tooltip = None
else:
App.get_running_app().root.add_widget(self.layout)
self.ctx.current_tooltip = self.layout
def on_leave(self):
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
@property
def ctx(self) -> CommonContext:
return App.get_running_app().ctx
class MissionLayout(GridLayout):
pass
@ -148,6 +188,9 @@ class Context(CommonContext):
mission_panel = None
last_checked_locations = {}
mission_id_to_button = {}
launching = False
refresh_from_launching = True
first_check = True
def __init__(self, ctx):
super().__init__(ctx)
@ -165,49 +208,87 @@ class Context(CommonContext):
return container
def build_mission_table(self, dt):
self.mission_panel.clear_widgets()
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
not self.refresh_from_launching)) or self.first_check:
self.refresh_from_launching = True
if self.ctx.mission_req_table:
self.mission_id_to_button = {}
categories = {}
available_missions = []
unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table,
self.ctx, available_missions=available_missions)
self.mission_panel.clear_widgets()
self.last_checked_locations = self.ctx.checked_locations
if self.ctx.mission_req_table:
self.last_checked_locations = self.ctx.checked_locations.copy()
self.first_check = False
# separate missions into categories
for mission in self.ctx.mission_req_table:
if not self.ctx.mission_req_table[mission].category in categories:
categories[self.ctx.mission_req_table[mission].category] = []
self.mission_id_to_button = {}
categories = {}
available_missions = []
unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table)
unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations,
self.ctx.mission_req_table,
self.ctx, available_missions=available_missions,
unfinished_locations=unfinished_locations)
categories[self.ctx.mission_req_table[mission].category].append(mission)
# separate missions into categories
for mission in self.ctx.mission_req_table:
if not self.ctx.mission_req_table[mission].category in categories:
categories[self.ctx.mission_req_table[mission].category] = []
for category in categories:
category_panel = MissionCategory()
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
categories[self.ctx.mission_req_table[mission].category].append(mission)
for mission in categories[category]:
text = mission
for category in categories:
category_panel = MissionCategory()
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]"
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
else:
text = f"[color=a9a9a9]{text}[/color]"
# Map is completed
for mission in categories[category]:
text = mission
tooltip = ""
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.bind(on_press=self.mission_callback)
self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button
category_panel.add_widget(mission_button)
# Map has uncollected locations
if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]"
category_panel.add_widget(Label(text=""))
self.mission_panel.add_widget(category_panel)
tooltip = f"Uncollected locations:\n"
tooltip += "\n".join(location for location in unfinished_locations[mission])
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
else:
text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: "
if len(self.ctx.mission_req_table[mission].required_world) > 0:
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for
req_mission in
self.ctx.mission_req_table[mission].required_world)
if self.ctx.mission_req_table[mission].number > 0:
tooltip += " and "
if self.ctx.mission_req_table[mission].number > 0:
tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback)
self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button
category_panel.add_widget(mission_button)
category_panel.add_widget(Label(text=""))
self.mission_panel.add_widget(category_panel)
elif self.launching:
self.refresh_from_launching = False
self.mission_panel.clear_widgets()
self.mission_panel.add_widget(Label(text="Launching Mission"))
def mission_callback(self, button):
self.ctx.play_mission(list(self.mission_id_to_button.keys())
[list(self.mission_id_to_button.values()).index(button)])
if not self.launching:
self.ctx.play_mission(list(self.mission_id_to_button.keys())
[list(self.mission_id_to_button.values()).index(button)])
self.launching = True
Clock.schedule_once(self.finish_launching, 10)
def finish_launching(self, dt):
self.launching = False
self.ui = SC2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
@ -215,7 +296,7 @@ class Context(CommonContext):
Builder.load_file(Utils.local_path(os.path.dirname(SC2WoLWorld.__file__), "Starcraft2.kv"))
async def shutdown(self):
await super(Context, self).shutdown()
await super(SC2Context, self).shutdown()
if self.sc2_run_task:
self.sc2_run_task.cancel()
@ -243,7 +324,7 @@ async def main():
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
args = parser.parse_args()
ctx = Context(args.connect, args.password)
ctx = SC2Context(args.connect, args.password)
ctx.auth = args.name
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
@ -267,6 +348,13 @@ maps_table = [
"ap_tvalerian01", "ap_tvalerian02a", "ap_tvalerian02b", "ap_tvalerian03"
]
wol_default_categories = [
"Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist",
"Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert",
"Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
"Char", "Char", "Char", "Char"
]
def calculate_items(items):
unit_unlocks = 0
@ -279,6 +367,7 @@ def calculate_items(items):
protoss_unlock = 0
minerals = 0
vespene = 0
supply = 0
for item in items:
data = lookup_id_to_name[item.item]
@ -303,9 +392,11 @@ def calculate_items(items):
minerals += item_table[data].number
elif item_table[data].type == "Vespene":
vespene += item_table[data].number
elif item_table[data].type == "Supply":
supply += item_table[data].number
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
lab_unlocks, protoss_unlock, minerals, vespene]
lab_unlocks, protoss_unlock, minerals, vespene, supply]
def calc_difficulty(difficulty):
@ -321,7 +412,7 @@ def calc_difficulty(difficulty):
return 'X'
async def starcraft_launch(ctx: Context, mission_id):
async def starcraft_launch(ctx: SC2Context, mission_id):
ctx.rec_announce_pos = len(ctx.items_rec_to_announce)
ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
ctx.announcements_pos = len(ctx.announcements)
@ -343,14 +434,14 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
sixth_bonus = False
seventh_bonus = False
eight_bonus = False
ctx: Context = None
ctx: SC2Context = None
mission_id = 0
can_read_game = False
last_received_update = 0
def __init__(self, ctx: Context, mission_id):
def __init__(self, ctx: SC2Context, mission_id):
self.ctx = ctx
self.mission_id = mission_id
@ -361,11 +452,11 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
if iteration == 0:
start_items = calculate_items(self.ctx.items_received)
difficulty = calc_difficulty(self.ctx.difficulty)
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {}".format(
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
difficulty,
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
start_items[5], start_items[6], start_items[7], start_items[8], start_items[9],
self.ctx.all_in_choice))
self.ctx.all_in_choice, start_items[10]))
self.last_received_update = len(self.ctx.items_received)
else:

View File

@ -141,8 +141,9 @@ item_table = {
"Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, progression=True),
"Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, progression=True),
"+5 Starting Minerals": ItemData(800+SC2WOL_ITEM_ID_OFFSET, "Minerals", 5, quantity=0, never_exclude=False),
"+5 Starting Vespene": ItemData(801+SC2WOL_ITEM_ID_OFFSET, "Vespene", 5, quantity=0, never_exclude=False)
"+15 Starting Minerals": ItemData(800+SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, never_exclude=False),
"+15 Starting Vespene": ItemData(801+SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, never_exclude=False),
"+2 Starting Supply": ItemData(802+SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, never_exclude=False),
}
basic_unit: typing.Tuple[str, ...] = (
@ -165,8 +166,8 @@ item_name_groups["Missions"] = ["Beat Liberation Day", "Beat The Outlaws", "Beat
"Beat Media Blitz", "Beat Piercing the Shroud"]
filler_items: typing.Tuple[str, ...] = (
'+5 Starting Minerals',
'+5 Starting Vespene'
'+15 Starting Minerals',
'+15 Starting Vespene'
)
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if data.code}

View File

@ -24,10 +24,10 @@ class FillMission(NamedTuple):
type: str
connect_to: List[int] # -1 connects to Menu
category: str
number: int = 0 # number of worlds need beaten
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
relegate: bool = False # true if this is a slot no build missions should be relegated to.
vanilla_shuffle_order = [
@ -37,7 +37,7 @@ vanilla_shuffle_order = [
FillMission("easy", [2], "Colonist"),
FillMission("medium", [3], "Colonist"),
FillMission("hard", [4], "Colonist", number=7),
FillMission("hard", [4], "Colonist", number=7),
FillMission("hard", [4], "Colonist", number=7, relegate=True),
FillMission("easy", [2], "Artifact", completion_critical=True),
FillMission("medium", [7], "Artifact", number=8, completion_critical=True),
FillMission("hard", [8], "Artifact", number=11, completion_critical=True),
@ -45,17 +45,17 @@ vanilla_shuffle_order = [
FillMission("hard", [10], "Artifact", completion_critical=True),
FillMission("medium", [2], "Covert", number=4),
FillMission("medium", [12], "Covert"),
FillMission("hard", [13], "Covert", number=8),
FillMission("hard", [13], "Covert", number=8),
FillMission("hard", [13], "Covert", number=8, relegate=True),
FillMission("hard", [13], "Covert", number=8, relegate=True),
FillMission("medium", [2], "Rebellion", number=6),
FillMission("hard", [16], "Rebellion"),
FillMission("hard", [17], "Rebellion"),
FillMission("hard", [18], "Rebellion"),
FillMission("hard", [19], "Rebellion"),
FillMission("hard", [19], "Rebellion", relegate=True),
FillMission("medium", [8], "Prophecy"),
FillMission("hard", [21], "Prophecy"),
FillMission("hard", [22], "Prophecy"),
FillMission("hard", [23], "Prophecy"),
FillMission("hard", [23], "Prophecy", relegate=True),
FillMission("hard", [11], "Char", completion_critical=True),
FillMission("hard", [25], "Char", completion_critical=True),
FillMission("hard", [25], "Char", completion_critical=True),

View File

@ -38,17 +38,25 @@ class AllInMap(Choice):
class MissionOrder(Choice):
"""Determines the order the missions are played in.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within"""
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within."""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is
not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete
the game."""
display_name = "Shuffle Protoss Missions"
class RelegateNoBuildMissions(DefaultOnToggle):
"""If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so
that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla."""
display_name = "Relegate No-Build Missions"
# noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
@ -56,7 +64,8 @@ sc2wol_options: Dict[str, Option] = {
"bunker_upgrade": BunkerUpgrade,
"all_in_map": AllInMap,
"mission_order": MissionOrder,
"shuffle_protoss": ShuffleProtoss
"shuffle_protoss": ShuffleProtoss,
"relegate_no_build": RelegateNoBuildMissions
}

View File

@ -132,6 +132,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
for mission in vanilla_shuffle_order:
if mission.type == "all_in":
missions.append("All-In")
elif get_option_value(world, player, "relegate_no_build") and mission.relegate:
missions.append("no_build")
else:
missions.append(mission.type)

View File

@ -33,6 +33,7 @@ class SC2WoLWorld(World):
game = "Starcraft 2 Wings of Liberty"
web = Starcraft2WoLWebWorld()
data_version = 2
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}