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): class StarcraftClientProcessor(ClientCommandProcessor):
ctx: Context ctx: SC2Context
def _cmd_disable_mission_check(self) -> bool: 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 """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 return True
class Context(CommonContext): class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor command_processor = StarcraftClientProcessor
game = "Starcraft 2 Wings of Liberty" game = "Starcraft 2 Wings of Liberty"
items_handling = 0b111 items_handling = 0b111
@ -89,10 +89,12 @@ class Context(CommonContext):
announcement_pos = 0 announcement_pos = 0
sc2_run_task: typing.Optional[asyncio.Task] = None sc2_run_task: typing.Optional[asyncio.Task] = None
missions_unlocked = False missions_unlocked = False
current_tooltip = None
last_loc_list = None
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password: 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: if not self.auth:
logger.info('Enter slot name:') logger.info('Enter slot name:')
self.auth = await self.console_input() self.auth = await self.console_input()
@ -105,6 +107,10 @@ class Context(CommonContext):
self.all_in_choice = args["slot_data"]["all_in_map"] self.all_in_choice = args["slot_data"]["all_in_map"]
slot_req_table = args["slot_data"]["mission_req"] slot_req_table = args["slot_data"]["mission_req"]
self.mission_req_table = {} 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: for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
@ -119,19 +125,53 @@ class Context(CommonContext):
self.announcements.append(args["data"]) self.announcements.append(args["data"])
def run_gui(self): 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.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder from kivy.lang import Builder
from kivy.uix.label import Label from kivy.uix.label import Label
from kivy.uix.button import Button from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import StringProperty
import Utils import Utils
class MissionButton(Button): class HoverableButton(HoverBehavior, Button):
pass 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): class MissionLayout(GridLayout):
pass pass
@ -148,6 +188,9 @@ class Context(CommonContext):
mission_panel = None mission_panel = None
last_checked_locations = {} last_checked_locations = {}
mission_id_to_button = {} mission_id_to_button = {}
launching = False
refresh_from_launching = True
first_check = True
def __init__(self, ctx): def __init__(self, ctx):
super().__init__(ctx) super().__init__(ctx)
@ -165,49 +208,87 @@ class Context(CommonContext):
return container return container
def build_mission_table(self, dt): 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_panel.clear_widgets()
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.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 self.mission_id_to_button = {}
for mission in self.ctx.mission_req_table: categories = {}
if not self.ctx.mission_req_table[mission].category in categories: available_missions = []
categories[self.ctx.mission_req_table[mission].category] = [] 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: categories[self.ctx.mission_req_table[mission].category].append(mission)
category_panel = MissionCategory()
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
for mission in categories[category]: for category in categories:
text = mission category_panel = MissionCategory()
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
if mission in unfinished_missions: # Map is completed
text = f"[color=6495ED]{text}[/color]" for mission in categories[category]:
elif mission in available_missions: text = mission
text = f"[color=FFFFFF]{text}[/color]" tooltip = ""
else:
text = f"[color=a9a9a9]{text}[/color]"
mission_button = MissionButton(text=text, size_hint_y=None, height=50) # Map has uncollected locations
mission_button.bind(on_press=self.mission_callback) if mission in unfinished_missions:
self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button text = f"[color=6495ED]{text}[/color]"
category_panel.add_widget(mission_button)
category_panel.add_widget(Label(text="")) tooltip = f"Uncollected locations:\n"
self.mission_panel.add_widget(category_panel) 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): def mission_callback(self, button):
self.ctx.play_mission(list(self.mission_id_to_button.keys()) if not self.launching:
[list(self.mission_id_to_button.values()).index(button)]) 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 = SC2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") 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")) Builder.load_file(Utils.local_path(os.path.dirname(SC2WoLWorld.__file__), "Starcraft2.kv"))
async def shutdown(self): async def shutdown(self):
await super(Context, self).shutdown() await super(SC2Context, self).shutdown()
if self.sc2_run_task: if self.sc2_run_task:
self.sc2_run_task.cancel() self.sc2_run_task.cancel()
@ -243,7 +324,7 @@ async def main():
parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument('--name', default=None, help="Slot Name to connect as.")
args = parser.parse_args() args = parser.parse_args()
ctx = Context(args.connect, args.password) ctx = SC2Context(args.connect, args.password)
ctx.auth = args.name ctx.auth = args.name
if ctx.server_task is None: if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") 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" "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): def calculate_items(items):
unit_unlocks = 0 unit_unlocks = 0
@ -279,6 +367,7 @@ def calculate_items(items):
protoss_unlock = 0 protoss_unlock = 0
minerals = 0 minerals = 0
vespene = 0 vespene = 0
supply = 0
for item in items: for item in items:
data = lookup_id_to_name[item.item] data = lookup_id_to_name[item.item]
@ -303,9 +392,11 @@ def calculate_items(items):
minerals += item_table[data].number minerals += item_table[data].number
elif item_table[data].type == "Vespene": elif item_table[data].type == "Vespene":
vespene += item_table[data].number 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, 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): def calc_difficulty(difficulty):
@ -321,7 +412,7 @@ def calc_difficulty(difficulty):
return 'X' 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.rec_announce_pos = len(ctx.items_rec_to_announce)
ctx.sent_announce_pos = len(ctx.items_sent_to_announce) ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
ctx.announcements_pos = len(ctx.announcements) ctx.announcements_pos = len(ctx.announcements)
@ -343,14 +434,14 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
sixth_bonus = False sixth_bonus = False
seventh_bonus = False seventh_bonus = False
eight_bonus = False eight_bonus = False
ctx: Context = None ctx: SC2Context = None
mission_id = 0 mission_id = 0
can_read_game = False can_read_game = False
last_received_update = 0 last_received_update = 0
def __init__(self, ctx: Context, mission_id): def __init__(self, ctx: SC2Context, mission_id):
self.ctx = ctx self.ctx = ctx
self.mission_id = mission_id self.mission_id = mission_id
@ -361,11 +452,11 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
if iteration == 0: if iteration == 0:
start_items = calculate_items(self.ctx.items_received) start_items = calculate_items(self.ctx.items_received)
difficulty = calc_difficulty(self.ctx.difficulty) difficulty = calc_difficulty(self.ctx.difficulty)
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {}".format( await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
difficulty, difficulty,
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], 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], 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) self.last_received_update = len(self.ctx.items_received)
else: else:

View File

@ -141,8 +141,9 @@ item_table = {
"Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, progression=True), "Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, progression=True),
"Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, 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), "+15 Starting Minerals": ItemData(800+SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, never_exclude=False),
"+5 Starting Vespene": ItemData(801+SC2WOL_ITEM_ID_OFFSET, "Vespene", 5, 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, ...] = ( 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"] "Beat Media Blitz", "Beat Piercing the Shroud"]
filler_items: typing.Tuple[str, ...] = ( filler_items: typing.Tuple[str, ...] = (
'+5 Starting Minerals', '+15 Starting Minerals',
'+5 Starting Vespene' '+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} 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 type: str
connect_to: List[int] # -1 connects to Menu connect_to: List[int] # -1 connects to Menu
category: str 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 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 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 = [ vanilla_shuffle_order = [
@ -37,7 +37,7 @@ vanilla_shuffle_order = [
FillMission("easy", [2], "Colonist"), FillMission("easy", [2], "Colonist"),
FillMission("medium", [3], "Colonist"), FillMission("medium", [3], "Colonist"),
FillMission("hard", [4], "Colonist", number=7), 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("easy", [2], "Artifact", completion_critical=True),
FillMission("medium", [7], "Artifact", number=8, completion_critical=True), FillMission("medium", [7], "Artifact", number=8, completion_critical=True),
FillMission("hard", [8], "Artifact", number=11, 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("hard", [10], "Artifact", completion_critical=True),
FillMission("medium", [2], "Covert", number=4), FillMission("medium", [2], "Covert", number=4),
FillMission("medium", [12], "Covert"), FillMission("medium", [12], "Covert"),
FillMission("hard", [13], "Covert", number=8), FillMission("hard", [13], "Covert", number=8, relegate=True),
FillMission("hard", [13], "Covert", number=8), FillMission("hard", [13], "Covert", number=8, relegate=True),
FillMission("medium", [2], "Rebellion", number=6), FillMission("medium", [2], "Rebellion", number=6),
FillMission("hard", [16], "Rebellion"), FillMission("hard", [16], "Rebellion"),
FillMission("hard", [17], "Rebellion"), FillMission("hard", [17], "Rebellion"),
FillMission("hard", [18], "Rebellion"), FillMission("hard", [18], "Rebellion"),
FillMission("hard", [19], "Rebellion"), FillMission("hard", [19], "Rebellion", relegate=True),
FillMission("medium", [8], "Prophecy"), FillMission("medium", [8], "Prophecy"),
FillMission("hard", [21], "Prophecy"), FillMission("hard", [21], "Prophecy"),
FillMission("hard", [22], "Prophecy"), FillMission("hard", [22], "Prophecy"),
FillMission("hard", [23], "Prophecy"), FillMission("hard", [23], "Prophecy", relegate=True),
FillMission("hard", [11], "Char", completion_critical=True), FillMission("hard", [11], "Char", completion_critical=True),
FillMission("hard", [25], "Char", completion_critical=True), FillMission("hard", [25], "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): class MissionOrder(Choice):
"""Determines the order the missions are played in. """Determines the order the missions are played in.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign. 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" display_name = "Mission Order"
option_vanilla = 0 option_vanilla = 0
option_vanilla_shuffled = 1 option_vanilla_shuffled = 1
class ShuffleProtoss(DefaultOnToggle): class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is """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 not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete
the game.""" the game."""
display_name = "Shuffle Protoss Missions" 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 # noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = { sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty, "game_difficulty": GameDifficulty,
@ -56,7 +64,8 @@ sc2wol_options: Dict[str, Option] = {
"bunker_upgrade": BunkerUpgrade, "bunker_upgrade": BunkerUpgrade,
"all_in_map": AllInMap, "all_in_map": AllInMap,
"mission_order": MissionOrder, "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: for mission in vanilla_shuffle_order:
if mission.type == "all_in": if mission.type == "all_in":
missions.append("All-In") missions.append("All-In")
elif get_option_value(world, player, "relegate_no_build") and mission.relegate:
missions.append("no_build")
else: else:
missions.append(mission.type) missions.append(mission.type)

View File

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