diff --git a/Launcher.py b/Launcher.py index 7d5b2f73..d5ade1f1 100644 --- a/Launcher.py +++ b/Launcher.py @@ -151,6 +151,8 @@ components: Iterable[Component] = ( Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 Component('Starcraft 2 Client', 'Starcraft2Client'), + # Wargroove + Component('Wargroove Client', 'WargrooveClient'), # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), diff --git a/Utils.py b/Utils.py index 010cc3e5..098d5f01 100644 --- a/Utils.py +++ b/Utils.py @@ -310,6 +310,9 @@ def get_default_options() -> OptionsType: "lufia2ac_options": { "rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc", }, + "wargroove_options": { + "root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" + } } return options diff --git a/WargrooveClient.py b/WargrooveClient.py new file mode 100644 index 00000000..fec20cc8 --- /dev/null +++ b/WargrooveClient.py @@ -0,0 +1,443 @@ +from __future__ import annotations +import os +import sys +import asyncio +import random +import shutil +from typing import Tuple, List, Iterable, Dict + +from worlds.wargroove import WargrooveWorld +from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData + +import ModuleUpdate +ModuleUpdate.update() + +import Utils +import json +import logging + +if __name__ == "__main__": + Utils.init_logging("WargrooveClient", exception_logger="Client") + +from NetUtils import NetworkItem, ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ + CommonContext, server_loop + +wg_logger = logging.getLogger("WG") + + +class WargrooveClientCommandProcessor(ClientCommandProcessor): + def _cmd_resync(self): + """Manually trigger a resync.""" + self.output(f"Syncing items.") + self.ctx.syncing = True + + def _cmd_commander(self, *commander_name: Iterable[str]): + """Set the current commander to the given commander.""" + if commander_name: + self.ctx.set_commander(' '.join(commander_name)) + else: + if self.ctx.can_choose_commander: + commanders = self.ctx.get_commanders() + wg_logger.info('Unlocked commanders: ' + + ', '.join((commander.name for commander, unlocked in commanders if unlocked))) + wg_logger.info('Locked commanders: ' + + ', '.join((commander.name for commander, unlocked in commanders if not unlocked))) + else: + wg_logger.error('Cannot set commanders in this game mode.') + + +class WargrooveContext(CommonContext): + command_processor: int = WargrooveClientCommandProcessor + game = "Wargroove" + items_handling = 0b111 # full remote + current_commander: CommanderData = faction_table["Starter"][0] + can_choose_commander: bool = False + commander_defense_boost_multiplier: int = 0 + income_boost_multiplier: int = 0 + starting_groove_multiplier: float + faction_item_ids = { + 'Starter': 0, + 'Cherrystone': 52025, + 'Felheim': 52026, + 'Floran': 52027, + 'Heavensong': 52028, + 'Requiem': 52029, + 'Outlaw': 52030 + } + buff_item_ids = { + 'Income Boost': 52023, + 'Commander Defense Boost': 52024, + } + + def __init__(self, server_address, password): + super(WargrooveContext, self).__init__(server_address, password) + self.send_index: int = 0 + self.syncing = False + self.awaiting_bridge = False + # self.game_communication_path: files go in this path to pass data between us and the actual game + if "appdata" in os.environ: + options = Utils.get_options() + root_directory = options["wargroove_options"]["root_directory"].replace("/", "\\") + data_directory = "lib\\worlds\\wargroove\\data\\" + dev_data_directory = "worlds\\wargroove\\data\\" + appdata_wargroove = os.path.expandvars("%APPDATA%\\Chucklefish\\Wargroove\\") + if not os.path.isfile(root_directory + "\\win64_bin\\wargroove64.exe"): + print_error_and_close("WargrooveClient couldn't find wargroove64.exe. " + "Unable to infer required game_communication_path") + self.game_communication_path = root_directory + "\\AP" + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + + if not os.path.isdir(appdata_wargroove): + print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!" + "Boot Wargroove and then close it to attempt to fix this error") + if not os.path.isdir(data_directory): + data_directory = dev_data_directory + if not os.path.isdir(data_directory): + print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!") + shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True) + else: + print_error_and_close("WargrooveClient couldn't detect system type. " + "Unable to infer required game_communication_path") + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(WargrooveContext, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + await super(WargrooveContext, self).connection_closed() + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root + "/" + file) + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + await super(WargrooveContext, self).shutdown() + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root+"/"+file) + + def on_package(self, cmd: str, args: dict): + if cmd in {"Connected"}: + filename = f"AP_settings.json" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + slot_data = args["slot_data"] + json.dump(args["slot_data"], f) + self.can_choose_commander = slot_data["can_choose_commander"] + print('can choose commander:', self.can_choose_commander) + self.starting_groove_multiplier = slot_data["starting_groove_multiplier"] + self.income_boost_multiplier = slot_data["income_boost"] + self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"] + f.close() + for ss in self.checked_locations: + filename = f"send{ss}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.close() + self.update_commander_data() + self.ui.update_tracker() + + random.seed(self.seed_name + str(self.slot)) + # Our indexes start at 1 and we have 24 levels + for i in range(1, 25): + filename = f"seed{i}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.write(str(random.randint(0, 4294967295))) + f.close() + + if cmd in {"RoomInfo"}: + self.seed_name = args["seed_name"] + + if cmd in {"ReceivedItems"}: + received_ids = [item.item for item in self.items_received] + for network_item in self.items_received: + filename = f"AP_{str(network_item.item)}.item" + path = os.path.join(self.game_communication_path, filename) + + # Newly-obtained items + if not os.path.isfile(path): + open(path, 'w').close() + # Announcing commander unlocks + item_name = self.item_names[network_item.item] + if item_name in faction_table.keys(): + for commander in faction_table[item_name]: + logger.info(f"{commander.name} has been unlocked!") + + with open(path, 'w') as f: + item_count = received_ids.count(network_item.item) + if self.buff_item_ids["Income Boost"] == network_item.item: + f.write(f"{item_count * self.income_boost_multiplier}") + elif self.buff_item_ids["Commander Defense Boost"] == network_item.item: + f.write(f"{item_count * self.commander_defense_boost_multiplier}") + else: + f.write(f"{item_count}") + f.close() + + print_filename = f"AP_{str(network_item.item)}.item.print" + print_path = os.path.join(self.game_communication_path, print_filename) + if not os.path.isfile(print_path): + open(print_path, 'w').close() + with open(print_path, 'w') as f: + f.write("Received " + + self.item_names[network_item.item] + + " from " + + self.player_names[network_item.player]) + f.close() + self.update_commander_data() + self.ui.update_tracker() + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + for ss in self.checked_locations: + filename = f"send{ss}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.close() + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager, HoverBehavior, ServerToolTip + from kivy.uix.tabbedpanel import TabbedPanelItem + from kivy.lang import Builder + from kivy.uix.button import Button + from kivy.uix.togglebutton import ToggleButton + from kivy.uix.boxlayout import BoxLayout + from kivy.uix.gridlayout import GridLayout + from kivy.uix.image import AsyncImage, Image + from kivy.uix.stacklayout import StackLayout + from kivy.uix.label import Label + from kivy.properties import ColorProperty + from kivy.uix.image import Image + import pkgutil + + class TrackerLayout(BoxLayout): + pass + + class CommanderSelect(BoxLayout): + pass + + class CommanderButton(ToggleButton): + pass + + class FactionBox(BoxLayout): + pass + + class CommanderGroup(BoxLayout): + pass + + class ItemTracker(BoxLayout): + pass + + class ItemLabel(Label): + pass + + class WargrooveManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("WG", "WG Console"), + ] + base_title = "Archipelago Wargroove Client" + ctx: WargrooveContext + unit_tracker: ItemTracker + trigger_tracker: BoxLayout + boost_tracker: BoxLayout + commander_buttons: Dict[int, List[CommanderButton]] + tracker_items = { + "Swordsman": ItemData(None, "Unit", False), + "Dog": ItemData(None, "Unit", False), + **item_table + } + + def build(self): + container = super().build() + panel = TabbedPanelItem(text="Wargroove") + panel.content = self.build_tracker() + self.tabs.add_widget(panel) + return container + + def build_tracker(self) -> TrackerLayout: + try: + tracker = TrackerLayout(orientation="horizontal") + commander_select = CommanderSelect(orientation="vertical") + self.commander_buttons = {} + + for faction, commanders in faction_table.items(): + faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70) + commander_group = CommanderGroup() + commander_buttons = [] + for commander in commanders: + commander_button = CommanderButton(text=commander.name, group="commanders") + if faction == "Starter": + commander_button.disabled = False + commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text)) + commander_buttons.append(commander_button) + commander_group.add_widget(commander_button) + self.commander_buttons[faction] = commander_buttons + faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10)) + faction_box.add_widget(commander_group) + commander_select.add_widget(faction_box) + item_tracker = ItemTracker(padding=[0,20]) + self.unit_tracker = BoxLayout(orientation="vertical") + other_tracker = BoxLayout(orientation="vertical") + self.trigger_tracker = BoxLayout(orientation="vertical") + self.boost_tracker = BoxLayout(orientation="vertical") + other_tracker.add_widget(self.trigger_tracker) + other_tracker.add_widget(self.boost_tracker) + item_tracker.add_widget(self.unit_tracker) + item_tracker.add_widget(other_tracker) + tracker.add_widget(commander_select) + tracker.add_widget(item_tracker) + self.update_tracker() + return tracker + except Exception as e: + print(e) + + def update_tracker(self): + received_ids = [item.item for item in self.ctx.items_received] + for faction, item_id in self.ctx.faction_item_ids.items(): + for commander_button in self.commander_buttons[faction]: + commander_button.disabled = not (faction == "Starter" or item_id in received_ids) + self.unit_tracker.clear_widgets() + self.trigger_tracker.clear_widgets() + for name, item in self.tracker_items.items(): + if item.type in ("Unit", "Trigger"): + status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1) + label = ItemLabel(text=name, color=status_color) + if item.type == "Unit": + self.unit_tracker.add_widget(label) + else: + self.trigger_tracker.add_widget(label) + self.boost_tracker.clear_widgets() + extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier + extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier + income_boost = ItemLabel(text="Extra Income: " + str(extra_income)) + defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense)) + self.boost_tracker.add_widget(income_boost) + self.boost_tracker.add_widget(defense_boost) + + self.ui = WargrooveManager(self) + data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode() + Builder.load_string(data) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + def update_commander_data(self): + if self.can_choose_commander: + faction_items = 0 + faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] + for network_item in self.items_received: + if self.item_names[network_item.item] in faction_item_names: + faction_items += 1 + starting_groove = (faction_items - 1) * self.starting_groove_multiplier + # Must be an integer larger than 0 + starting_groove = int(max(starting_groove, 0)) + data = { + "commander": self.current_commander.internal_name, + "starting_groove": starting_groove + } + else: + data = { + "commander": "seed", + "starting_groove": 0 + } + filename = 'commander.json' + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + json.dump(data, f) + if self.ui: + self.ui.update_tracker() + + def set_commander(self, commander_name: str) -> bool: + """Sets the current commander to the given one, if possible""" + if not self.can_choose_commander: + wg_logger.error("Cannot set commanders in this game mode.") + return + match_name = commander_name.lower() + for commander, unlocked in self.get_commanders(): + if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name: + if unlocked: + self.current_commander = commander + self.syncing = True + wg_logger.info(f"Commander set to {commander.name}.") + self.update_commander_data() + return True + else: + wg_logger.error(f"Commander {commander.name} has not been unlocked.") + return False + else: + wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.") + + def get_commanders(self) -> List[Tuple[CommanderData, bool]]: + """Gets a list of commanders with their unlocked status""" + commanders = [] + received_ids = [item.item for item in self.items_received] + for faction in faction_table.keys(): + unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids + commanders += [(commander, unlocked) for commander in faction_table[faction]] + return commanders + + +async def game_watcher(ctx: WargrooveContext): + from worlds.wargroove.Locations import location_table + while not ctx.exit_event.is_set(): + if ctx.syncing == True: + sync_msg = [{'cmd': 'Sync'}] + if ctx.locations_checked: + sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) + await ctx.send_msgs(sync_msg) + ctx.syncing = False + sending = [] + victory = False + for root, dirs, files in os.walk(ctx.game_communication_path): + for file in files: + if file.find("send") > -1: + st = file.split("send", -1)[1] + sending = sending+[(int(st))] + if file.find("victory") > -1: + victory = True + ctx.locations_checked = sending + message = [{"cmd": 'LocationChecks', "locations": sending}] + await ctx.send_msgs(message) + if not ctx.finished_game and victory: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + await asyncio.sleep(0.1) + + +def print_error_and_close(msg): + logger.error("Error: " + msg) + Utils.messagebox("Error", msg, error=True) + sys.exit(1) + +if __name__ == '__main__': + async def main(args): + ctx = WargrooveContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + game_watcher(ctx), name="WargrooveProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + import colorama + + parser = get_base_parser(description="Wargroove Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/host.yaml b/host.yaml index ce242fd4..78fff669 100644 --- a/host.yaml +++ b/host.yaml @@ -139,6 +139,12 @@ pokemon_rb_options: # True for operating system default program # Alternatively, a path to a program to open the .gb file with rom_start: true + +wargroove_options: + # Locate the Wargroove root directory on your system. + # This is used by the Wargroove client, so it knows where to send communication files to + root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" + zillion_options: # File name of the Zillion US rom rom_file: "Zillion (UE) [!].sms" diff --git a/worlds/wargroove/Items.py b/worlds/wargroove/Items.py new file mode 100644 index 00000000..acb31a84 --- /dev/null +++ b/worlds/wargroove/Items.py @@ -0,0 +1,104 @@ +import typing + +from BaseClasses import Item, ItemClassification +from typing import Dict, List + +PROGRESSION = ItemClassification.progression +PROGRESSION_SKIP_BALANCING = ItemClassification.progression_skip_balancing +USEFUL = ItemClassification.useful +FILLER = ItemClassification.filler + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + type: str + classification: ItemClassification = PROGRESSION + + +item_table: Dict[str, ItemData] = { + # Units + 'Spearman': ItemData(52000, 'Unit'), + 'Wagon': ItemData(52001, 'Unit', USEFUL), + 'Mage': ItemData(52002, 'Unit'), + 'Archer': ItemData(52003, 'Unit'), + 'Knight': ItemData(52004, 'Unit'), + 'Ballista': ItemData(52005, 'Unit'), + 'Golem': ItemData(52006, 'Unit', USEFUL), + 'Harpy': ItemData(52007, 'Unit'), + 'Witch': ItemData(52008, 'Unit', USEFUL), + 'Dragon': ItemData(52009, 'Unit'), + 'Balloon': ItemData(52010, 'Unit', USEFUL), + 'Barge': ItemData(52011, 'Unit'), + 'Merfolk': ItemData(52012, 'Unit'), + 'Turtle': ItemData(52013, 'Unit'), + 'Harpoon Ship': ItemData(52014, 'Unit'), + 'Warship': ItemData(52015, 'Unit'), + 'Thief': ItemData(52016, 'Unit'), + 'Rifleman': ItemData(52017, 'Unit'), + + # Map Triggers + 'Eastern Bridges': ItemData(52018, 'Trigger'), + 'Southern Walls': ItemData(52019, 'Trigger'), + 'Final Bridges': ItemData(52020, 'Trigger', PROGRESSION_SKIP_BALANCING), + 'Final Walls': ItemData(52021, 'Trigger', PROGRESSION_SKIP_BALANCING), + 'Final Sickle': ItemData(52022, 'Trigger', PROGRESSION_SKIP_BALANCING), + + # Player Buffs + 'Income Boost': ItemData(52023, 'Boost', FILLER), + + 'Commander Defense Boost': ItemData(52024, 'Boost', FILLER), + + # Factions + 'Cherrystone Commanders': ItemData(52025, 'Faction', USEFUL), + 'Felheim Commanders': ItemData(52026, 'Faction', USEFUL), + 'Floran Commanders': ItemData(52027, 'Faction', USEFUL), + 'Heavensong Commanders': ItemData(52028, 'Faction', USEFUL), + 'Requiem Commanders': ItemData(52029, 'Faction', USEFUL), + 'Outlaw Commanders': ItemData(52030, 'Faction', USEFUL), + + # Event Items + 'Wargroove Victory': ItemData(None, 'Goal') + +} + + +class CommanderData(typing.NamedTuple): + name: str + internal_name: str + alt_name: str = None + + +faction_table: Dict[str, List[CommanderData]] = { + 'Starter': [ + CommanderData('Mercival', 'commander_mercival') + ], + 'Cherrystone': [ + CommanderData('Mercia', 'commander_mercia'), + CommanderData('Emeric', 'commander_emeric'), + CommanderData('Caesar', 'commander_caesar'), + ], + 'Felheim': [ + CommanderData('Valder', 'commander_valder'), + CommanderData('Ragna', 'commander_ragna'), + CommanderData('Sigrid', 'commander_sigrid') + ], + 'Floran': [ + CommanderData('Greenfinger', 'commander_greenfinger'), + CommanderData('Sedge', 'commander_sedge'), + CommanderData('Nuru', 'commander_nuru') + ], + 'Heavensong': [ + CommanderData('Tenri', 'commander_tenri'), + CommanderData('Koji', 'commander_koji'), + CommanderData('Ryota', 'commander_ryota') + ], + 'Requiem': [ + CommanderData('Elodie', 'commander_elodie'), + CommanderData('Dark Mercia', 'commander_darkmercia') + ], + 'Outlaw': [ + CommanderData('Wulfar', 'commander_wulfar'), + CommanderData('Twins', 'commander_twins', 'Errol & Orla'), + CommanderData('Vesper', 'commander_vesper') + ] +} \ No newline at end of file diff --git a/worlds/wargroove/Locations.py b/worlds/wargroove/Locations.py new file mode 100644 index 00000000..e9fe52a1 --- /dev/null +++ b/worlds/wargroove/Locations.py @@ -0,0 +1,41 @@ +location_table = { + 'Humble Beginnings: Caesar': 53001, + 'Humble Beginnings: Chest 1': 53002, + 'Humble Beginnings: Chest 2': 53003, + 'Humble Beginnings: Victory': 53004, + 'Best Friendssss: Find Sedge': 53005, + 'Best Friendssss: Victory': 53006, + 'A Knight\'s Folly: Caesar': 53007, + 'A Knight\'s Folly: Victory': 53008, + 'Denrunaway: Chest': 53009, + 'Denrunaway: Victory': 53010, + 'Dragon Freeway: Victory': 53011, + 'Deep Thicket: Find Sedge': 53012, + 'Deep Thicket: Victory': 53013, + 'Corrupted Inlet: Victory': 53014, + 'Mage Mayhem: Caesar': 53015, + 'Mage Mayhem: Victory': 53016, + 'Endless Knight: Victory': 53017, + 'Ambushed in the Middle: Victory (Blue)': 53018, + 'Ambushed in the Middle: Victory (Green)': 53019, + 'The Churning Sea: Victory': 53020, + 'Frigid Archery: Light the Torch': 53021, + 'Frigid Archery: Victory': 53022, + 'Archery Lessons: Chest': 53023, + 'Archery Lessons: Victory': 53024, + 'Surrounded: Caesar': 53025, + 'Surrounded: Victory': 53026, + 'Darkest Knight: Victory': 53027, + 'Robbed: Victory': 53028, + 'Open Season: Caesar': 53029, + 'Open Season: Victory': 53030, + 'Doggo Mountain: Find all the Dogs': 53031, + 'Doggo Mountain: Victory': 53032, + 'Tenri\'s Fall: Victory': 53033, + 'Master of the Lake: Victory': 53034, + 'A Ballista\'s Revenge: Victory': 53035, + 'Rebel Village: Victory (Pink)': 53036, + 'Rebel Village: Victory (Red)': 53037, + 'Foolish Canal: Victory': 53038, + 'Wargroove Finale: Victory': None, +} diff --git a/worlds/wargroove/Options.py b/worlds/wargroove/Options.py new file mode 100644 index 00000000..c8b8b37e --- /dev/null +++ b/worlds/wargroove/Options.py @@ -0,0 +1,38 @@ +import typing +from Options import Choice, Option, Range + + +class IncomeBoost(Range): + """How much extra income the player gets per turn per boost received.""" + display_name = "Income Boost" + range_start = 0 + range_end = 100 + default = 25 + + +class CommanderDefenseBoost(Range): + """How much extra defense the player's commander gets per boost received.""" + display_name = "Commander Defense Boost" + range_start = 0 + range_end = 8 + default = 2 + + +class CommanderChoice(Choice): + """How the player's commander is selected for missions. + Locked Random: The player's commander is randomly predetermined for each level. + Unlockable Factions: The player starts with Mercival and can unlock playable factions. + Random Starting Faction: The player starts with a random starting faction and can unlock the rest. + When playing with unlockable factions, faction items are added to the pool. + Extra faction items after the first also reward starting Groove charge.""" + display_name = "Commander Choice" + option_locked_random = 0 + option_unlockable_factions = 1 + option_random_starting_faction = 2 + + +wargroove_options: typing.Dict[str, type(Option)] = { + "income_boost": IncomeBoost, + "commander_defense_boost": CommanderDefenseBoost, + "commander_choice": CommanderChoice +} diff --git a/worlds/wargroove/Regions.py b/worlds/wargroove/Regions.py new file mode 100644 index 00000000..02f5ab87 --- /dev/null +++ b/worlds/wargroove/Regions.py @@ -0,0 +1,169 @@ +def create_regions(world, player: int): + from . import create_region + from .Locations import location_table + + world.regions += [ + create_region(world, player, 'Menu', None, ['Humble Beginnings']), + # Level 1 + create_region(world, player, 'Humble Beginnings', [ + 'Humble Beginnings: Caesar', + 'Humble Beginnings: Chest 1', + 'Humble Beginnings: Chest 2', + 'Humble Beginnings: Victory', + ], ['Best Friendssss', 'A Knight\'s Folly', 'Denrunaway', 'Wargroove Finale']), + + # Levels 2A-2C + create_region(world, player, 'Best Friendssss', [ + 'Best Friendssss: Find Sedge', + 'Best Friendssss: Victory' + ], ['Dragon Freeway', 'Deep Thicket', 'Corrupted Inlet']), + + create_region(world, player, 'A Knight\'s Folly', [ + 'A Knight\'s Folly: Caesar', + 'A Knight\'s Folly: Victory' + ], ['Mage Mayhem', 'Endless Knight', 'Ambushed in the Middle']), + + create_region(world, player, 'Denrunaway', [ + 'Denrunaway: Chest', + 'Denrunaway: Victory' + ], ['The Churning Sea', 'Frigid Archery', 'Archery Lessons']), + + # Levels 3AA-3AC + create_region(world, player, 'Dragon Freeway', [ + 'Dragon Freeway: Victory', + ], ['Surrounded']), + + create_region(world, player, 'Deep Thicket', [ + 'Deep Thicket: Find Sedge', + 'Deep Thicket: Victory', + ], ['Darkest Knight']), + + create_region(world, player, 'Corrupted Inlet', [ + 'Corrupted Inlet: Victory', + ], ['Robbed']), + + # Levels 3BA-3BC + create_region(world, player, 'Mage Mayhem', [ + 'Mage Mayhem: Caesar', + 'Mage Mayhem: Victory', + ], ['Open Season', 'Foolish Canal: Mage Mayhem Entrance']), + + create_region(world, player, 'Endless Knight', [ + 'Endless Knight: Victory', + ], ['Doggo Mountain', 'Foolish Canal: Endless Knight Entrance']), + + create_region(world, player, 'Ambushed in the Middle', [ + 'Ambushed in the Middle: Victory (Blue)', + 'Ambushed in the Middle: Victory (Green)', + ], ['Tenri\'s Fall']), + + # Levels 3CA-3CC + create_region(world, player, 'The Churning Sea', [ + 'The Churning Sea: Victory', + ], ['Rebel Village']), + + create_region(world, player, 'Frigid Archery', [ + 'Frigid Archery: Light the Torch', + 'Frigid Archery: Victory', + ], ['A Ballista\'s Revenge']), + + create_region(world, player, 'Archery Lessons', [ + 'Archery Lessons: Chest', + 'Archery Lessons: Victory', + ], ['Master of the Lake']), + + # Levels 4AA-4AC + create_region(world, player, 'Surrounded', [ + 'Surrounded: Caesar', + 'Surrounded: Victory', + ]), + + create_region(world, player, 'Darkest Knight', [ + 'Darkest Knight: Victory', + ]), + + create_region(world, player, 'Robbed', [ + 'Robbed: Victory', + ]), + + # Levels 4BAA-4BCA + create_region(world, player, 'Open Season', [ + 'Open Season: Caesar', + 'Open Season: Victory', + ]), + + create_region(world, player, 'Doggo Mountain', [ + 'Doggo Mountain: Find all the Dogs', + 'Doggo Mountain: Victory', + ]), + + create_region(world, player, 'Tenri\'s Fall', [ + 'Tenri\'s Fall: Victory', + ]), + + # Level 4BAB + create_region(world, player, 'Foolish Canal', [ + 'Foolish Canal: Victory', + ]), + + # Levels 4CA-4CC + create_region(world, player, 'Master of the Lake', [ + 'Master of the Lake: Victory', + ]), + + create_region(world, player, 'A Ballista\'s Revenge', [ + 'A Ballista\'s Revenge: Victory', + ]), + + create_region(world, player, 'Rebel Village', [ + 'Rebel Village: Victory (Pink)', + 'Rebel Village: Victory (Red)', + ]), + + # Final Level + create_region(world, player, 'Wargroove Finale', [ + 'Wargroove Finale: Victory' + ]), + ] + + # link up our regions with the entrances + world.get_entrance('Humble Beginnings', player).connect(world.get_region('Humble Beginnings', player)) + world.get_entrance('Best Friendssss', player).connect(world.get_region('Best Friendssss', player)) + world.get_entrance('A Knight\'s Folly', player).connect(world.get_region('A Knight\'s Folly', player)) + world.get_entrance('Denrunaway', player).connect(world.get_region('Denrunaway', player)) + world.get_entrance('Wargroove Finale', player).connect(world.get_region('Wargroove Finale', player)) + + world.get_entrance('Dragon Freeway', player).connect(world.get_region('Dragon Freeway', player)) + world.get_entrance('Deep Thicket', player).connect(world.get_region('Deep Thicket', player)) + world.get_entrance('Corrupted Inlet', player).connect(world.get_region('Corrupted Inlet', player)) + + world.get_entrance('Mage Mayhem', player).connect(world.get_region('Mage Mayhem', player)) + world.get_entrance('Endless Knight', player).connect(world.get_region('Endless Knight', player)) + world.get_entrance('Ambushed in the Middle', player).connect(world.get_region('Ambushed in the Middle', player)) + + world.get_entrance('The Churning Sea', player).connect(world.get_region('The Churning Sea', player)) + world.get_entrance('Frigid Archery', player).connect(world.get_region('Frigid Archery', player)) + world.get_entrance('Archery Lessons', player).connect(world.get_region('Archery Lessons', player)) + + world.get_entrance('Surrounded', player).connect(world.get_region('Surrounded', player)) + + world.get_entrance('Darkest Knight', player).connect(world.get_region('Darkest Knight', player)) + + world.get_entrance('Robbed', player).connect(world.get_region('Robbed', player)) + + world.get_entrance('Open Season', player).connect(world.get_region('Open Season', player)) + + world.get_entrance('Doggo Mountain', player).connect(world.get_region('Doggo Mountain', player)) + + world.get_entrance('Tenri\'s Fall', player).connect(world.get_region('Tenri\'s Fall', player)) + + world.get_entrance('Foolish Canal: Mage Mayhem Entrance', player).connect(world.get_region('Foolish Canal', player)) + world.get_entrance('Foolish Canal: Endless Knight Entrance', player).connect( + world.get_region('Foolish Canal', player) + ) + + world.get_entrance('Master of the Lake', player).connect(world.get_region('Master of the Lake', player)) + + world.get_entrance('A Ballista\'s Revenge', player).connect(world.get_region('A Ballista\'s Revenge', player)) + + world.get_entrance('Rebel Village', player).connect(world.get_region('Rebel Village', player)) diff --git a/worlds/wargroove/Rules.py b/worlds/wargroove/Rules.py new file mode 100644 index 00000000..e1633773 --- /dev/null +++ b/worlds/wargroove/Rules.py @@ -0,0 +1,161 @@ +from typing import List + +from BaseClasses import MultiWorld, Region, Location +from ..AutoWorld import LogicMixin +from ..generic.Rules import set_rule + + +class WargrooveLogic(LogicMixin): + def _wargroove_has_item(self, player: int, item: str) -> bool: + return self.has(item, player) + + def _wargroove_has_region(self, player: int, region: str) -> bool: + return self.can_reach(region, 'Region', player) + + def _wargroove_has_item_and_region(self, player: int, item: str, region: str) -> bool: + return self.can_reach(region, 'Region', player) and self.has(item, player) + + +def set_rules(world: MultiWorld, player: int): + # Final Level + set_rule(world.get_location('Wargroove Finale: Victory', player), + lambda state: state._wargroove_has_item(player, "Final Bridges") and + state._wargroove_has_item(player, "Final Walls") and + state._wargroove_has_item(player, "Final Sickle")) + # Level 1 + set_rule(world.get_location('Humble Beginnings: Caesar', player), lambda state: True) + set_rule(world.get_location('Humble Beginnings: Chest 1', player), lambda state: True) + set_rule(world.get_location('Humble Beginnings: Chest 2', player), lambda state: True) + set_rule(world.get_location('Humble Beginnings: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('Humble Beginnings', player), + [world.get_location('Humble Beginnings: Victory', player)]) + + # Levels 2A-2C + set_rule(world.get_location('Best Friendssss: Find Sedge', player), lambda state: True) + set_rule(world.get_location('Best Friendssss: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('Best Friendssss', player), + [world.get_location('Best Friendssss: Victory', player)]) + + set_rule(world.get_location('A Knight\'s Folly: Caesar', player), lambda state: True) + set_rule(world.get_location('A Knight\'s Folly: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('A Knight\'s Folly', player), + [world.get_location('A Knight\'s Folly: Victory', player)]) + + set_rule(world.get_location('Denrunaway: Chest', player), lambda state: True) + set_rule(world.get_location('Denrunaway: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('Denrunaway', player), [world.get_location('Denrunaway: Victory', player)]) + + # Levels 3AA-3AC + set_rule(world.get_location('Dragon Freeway: Victory', player), + lambda state: state._wargroove_has_item(player, 'Mage')) + set_region_exit_rules(world.get_region('Dragon Freeway', player), + [world.get_location('Dragon Freeway: Victory', player)]) + + set_rule(world.get_location('Deep Thicket: Find Sedge', player), + lambda state: state._wargroove_has_item(player, 'Mage')) + set_rule(world.get_location('Deep Thicket: Victory', player), + lambda state: state._wargroove_has_item(player, 'Mage')) + set_region_exit_rules(world.get_region('Deep Thicket', player), + [world.get_location('Deep Thicket: Victory', player)]) + + set_rule(world.get_location('Corrupted Inlet: Victory', player), + lambda state: state._wargroove_has_item(player, 'Barge') or + state._wargroove_has_item(player, 'Merfolk') or + state._wargroove_has_item(player, 'Warship')) + set_region_exit_rules(world.get_region('Corrupted Inlet', player), + [world.get_location('Corrupted Inlet: Victory', player)]) + + # Levels 3BA-3BC + set_rule(world.get_location('Mage Mayhem: Caesar', player), + lambda state: state._wargroove_has_item(player, 'Harpy') or state._wargroove_has_item(player, 'Dragon')) + set_rule(world.get_location('Mage Mayhem: Victory', player), + lambda state: state._wargroove_has_item(player, 'Harpy') or state._wargroove_has_item(player, 'Dragon')) + set_region_exit_rules(world.get_region('Mage Mayhem', player), [world.get_location('Mage Mayhem: Victory', player)]) + + set_rule(world.get_location('Endless Knight: Victory', player), + lambda state: state._wargroove_has_item(player, 'Eastern Bridges') and ( + state._wargroove_has_item(player, 'Spearman') or + state._wargroove_has_item(player, 'Harpy') or + state._wargroove_has_item(player, 'Dragon'))) + set_region_exit_rules(world.get_region('Endless Knight', player), + [world.get_location('Endless Knight: Victory', player)]) + + set_rule(world.get_location('Ambushed in the Middle: Victory (Blue)', player), + lambda state: state._wargroove_has_item(player, 'Spearman')) + set_rule(world.get_location('Ambushed in the Middle: Victory (Green)', player), + lambda state: state._wargroove_has_item(player, 'Spearman')) + set_region_exit_rules(world.get_region('Ambushed in the Middle', player), + [world.get_location('Ambushed in the Middle: Victory (Blue)', player), + world.get_location('Ambushed in the Middle: Victory (Green)', player)]) + + # Levels 3CA-3CC + set_rule(world.get_location('The Churning Sea: Victory', player), + lambda state: (state._wargroove_has_item(player, 'Merfolk') or state._wargroove_has_item(player, 'Turtle')) + and state._wargroove_has_item(player, 'Harpoon Ship')) + set_region_exit_rules(world.get_region('The Churning Sea', player), + [world.get_location('The Churning Sea: Victory', player)]) + + set_rule(world.get_location('Frigid Archery: Light the Torch', player), + lambda state: state._wargroove_has_item(player, 'Archer') and + state._wargroove_has_item(player, 'Southern Walls')) + set_rule(world.get_location('Frigid Archery: Victory', player), + lambda state: state._wargroove_has_item(player, 'Archer')) + set_region_exit_rules(world.get_region('Frigid Archery', player), + [world.get_location('Frigid Archery: Victory', player)]) + + set_rule(world.get_location('Archery Lessons: Chest', player), + lambda state: state._wargroove_has_item(player, 'Knight') and + state._wargroove_has_item(player, 'Southern Walls')) + set_rule(world.get_location('Archery Lessons: Victory', player), + lambda state: state._wargroove_has_item(player, 'Knight') and + state._wargroove_has_item(player, 'Southern Walls')) + set_region_exit_rules(world.get_region('Archery Lessons', player), + [world.get_location('Archery Lessons: Victory', player)]) + + # Levels 4AA-4AC + set_rule(world.get_location('Surrounded: Caesar', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Surrounded')) + set_rule(world.get_location('Surrounded: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Surrounded')) + set_rule(world.get_location('Darkest Knight: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Darkest Knight')) + set_rule(world.get_location('Robbed: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Thief', 'Robbed') and + state._wargroove_has_item(player, 'Rifleman')) + + # Levels 4BA-4BC + set_rule(world.get_location('Open Season: Caesar', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Open Season') and + state._wargroove_has_item(player, 'Knight')) + set_rule(world.get_location('Open Season: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Open Season') and + state._wargroove_has_item(player, 'Knight')) + set_rule(world.get_location('Doggo Mountain: Find all the Dogs', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Doggo Mountain')) + set_rule(world.get_location('Doggo Mountain: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Doggo Mountain')) + set_rule(world.get_location('Tenri\'s Fall: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Tenri\'s Fall') and + state._wargroove_has_item(player, 'Thief')) + set_rule(world.get_location('Foolish Canal: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Foolish Canal') and + state._wargroove_has_item(player, 'Spearman')) + + # Levels 4CA-4CC + set_rule(world.get_location('Master of the Lake: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Warship', 'Master of the Lake')) + set_rule(world.get_location('A Ballista\'s Revenge: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Ballista', 'A Ballista\'s Revenge')) + set_rule(world.get_location('Rebel Village: Victory (Pink)', player), + lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Rebel Village')) + set_rule(world.get_location('Rebel Village: Victory (Red)', player), + lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Rebel Village')) + + +def set_region_exit_rules(region: Region, locations: List[Location], operator: str = "or"): + if operator == "or": + exit_rule = lambda state: any(location.access_rule(state) for location in locations) + else: + exit_rule = lambda state: all(location.access_rule(state) for location in locations) + for region_exit in region.exits: + region_exit.access_rule = exit_rule diff --git a/worlds/wargroove/Wargroove.kv b/worlds/wargroove/Wargroove.kv new file mode 100644 index 00000000..9609684a --- /dev/null +++ b/worlds/wargroove/Wargroove.kv @@ -0,0 +1,28 @@ +: + orientation: 'vertical' + padding: [10,5,10,5] + size_hint_y: 0.14 + +: + orientation: 'horizontal' + +: + text_size: self.size + size_hint: (None, 0.8) + width: 100 + markup: True + halign: 'center' + valign: 'middle' + padding_x: 5 + outline_width: 1 + disabled: True + on_release: setattr(self, 'state', 'down') + +: + orientation: 'horizontal' + padding_y: 5 + +: + size_hint_x: None + size: self.texture_size + pos_hint: {'left': 1} \ No newline at end of file diff --git a/worlds/wargroove/__init__.py b/worlds/wargroove/__init__.py new file mode 100644 index 00000000..ca387c41 --- /dev/null +++ b/worlds/wargroove/__init__.py @@ -0,0 +1,139 @@ +import os +import string +import json + +from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification +from .Items import item_table, faction_table +from .Locations import location_table +from .Regions import create_regions +from .Rules import set_rules +from ..AutoWorld import World, WebWorld +from .Options import wargroove_options + + +class WargrooveWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Wargroove for Archipelago.", + "English", + "wargroove_en.md", + "wargroove/en", + ["Fly Sniper"] + )] + + +class WargrooveWorld(World): + """ + Command an army, in this retro style turn based strategy game! + """ + + option_definitions = wargroove_options + game = "Wargroove" + topology_present = True + data_version = 1 + web = WargrooveWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + def _get_slot_data(self): + return { + 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)), + 'income_boost': self.multiworld.income_boost[self.player], + 'commander_defense_boost': self.multiworld.commander_defense_boost[self.player], + 'can_choose_commander': self.multiworld.commander_choice[self.player] != 0, + 'starting_groove_multiplier': 20 # Backwards compatibility in case this ever becomes an option + } + + def generate_early(self): + # Selecting a random starting faction + if self.multiworld.commander_choice[self.player] == 2: + factions = [faction for faction in faction_table.keys() if faction != "Starter"] + starting_faction = WargrooveItem(self.multiworld.random.choice(factions) + ' Commanders', self.player) + self.multiworld.push_precollected(starting_faction) + + def generate_basic(self): + # Fill out our pool with our items from the item table + pool = [] + precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} + ignore_faction_items = self.multiworld.commander_choice[self.player] == 0 + for name, data in item_table.items(): + if data.code is not None and name not in precollected_item_names and not data.classification == ItemClassification.filler: + if name.endswith(' Commanders') and ignore_faction_items: + continue + item = WargrooveItem(name, self.player) + pool.append(item) + + # Matching number of unfilled locations with filler items + locations_remaining = len(location_table) - 1 - len(pool) + while locations_remaining > 0: + # Filling the pool equally with both types of filler items + pool.append(WargrooveItem("Commander Defense Boost", self.player)) + locations_remaining -= 1 + if locations_remaining > 0: + pool.append(WargrooveItem("Income Boost", self.player)) + locations_remaining -= 1 + + self.multiworld.itempool += pool + + # Placing victory event at final location + victory = WargrooveItem("Wargroove Victory", self.player) + self.multiworld.get_location("Wargroove Finale: Victory", self.player).place_locked_item(victory) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Wargroove Victory", self.player) + + def set_rules(self): + set_rules(self.multiworld, self.player) + + def create_item(self, name: str) -> Item: + return WargrooveItem(name, self.player) + + def create_regions(self): + create_regions(self.multiworld, self.player) + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in wargroove_options: + option = getattr(self.multiworld, option_name)[self.player] + slot_data[option_name] = int(option.value) + return slot_data + + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(["Commander Defense Boost", "Income Boost"]) + + +def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): + ret = Region(name, player, world) + if locations: + for location in locations: + loc_id = location_table.get(location, 0) + location = WargrooveLocation(player, location, loc_id, ret) + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + + return ret + + +class WargrooveLocation(Location): + game: str = "Wargroove" + + def __init__(self, player: int, name: str, address=None, parent=None): + super(WargrooveLocation, self).__init__(player, name, address, parent) + if address is None: + self.event = True + self.locked = True + + +class WargrooveItem(Item): + game = "Wargroove" + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(WargrooveItem, self).__init__( + name, + item_data.classification, + item_data.code, + player + ) diff --git a/worlds/wargroove/data/mods/ArchipelagoMod/maps.dat b/worlds/wargroove/data/mods/ArchipelagoMod/maps.dat new file mode 100644 index 00000000..3e1aeae2 Binary files /dev/null and b/worlds/wargroove/data/mods/ArchipelagoMod/maps.dat differ diff --git a/worlds/wargroove/data/mods/ArchipelagoMod/mod.dat b/worlds/wargroove/data/mods/ArchipelagoMod/mod.dat new file mode 100644 index 00000000..8b136edd Binary files /dev/null and b/worlds/wargroove/data/mods/ArchipelagoMod/mod.dat differ diff --git a/worlds/wargroove/data/mods/ArchipelagoMod/modAssets.dat b/worlds/wargroove/data/mods/ArchipelagoMod/modAssets.dat new file mode 100644 index 00000000..37dc5421 Binary files /dev/null and b/worlds/wargroove/data/mods/ArchipelagoMod/modAssets.dat differ diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp new file mode 100644 index 00000000..cf0c5304 Binary files /dev/null and b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp differ diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak new file mode 100644 index 00000000..5eb2c244 Binary files /dev/null and b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak differ diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md new file mode 100644 index 00000000..18474a42 --- /dev/null +++ b/worlds/wargroove/docs/en_Wargroove.md @@ -0,0 +1,34 @@ +# Wargroove (Steam, Windows) + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +This randomizer shuffles units, map events, factions and boosts. It features a custom, non-linear campaign where the +final level and 3 branching paths are all available to the player from the start. The player cannot beat the final level +without specific items scattered throughout the branching paths. Certain levels on these paths may require +specific units or items in order to progress. + +## What items and locations get shuffled? + +1. Every buildable unit in the game (except for soldiers and dogs, which are free). +2. Commanders available to certain factions. If the player acquires the Floran Commanders, they can select any commander +from that faction. +3. Income and Commander Defense boosts that provide the player with extra income or extra commander defense. +4. Special map events like the Eastern Bridges or the Southern Walls, which unlock certain locations in certain levels. + +## Which items can be in another player's world? + +Any of the above items can be in another player's world. + +## When the player receives an item, what happens? + +When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action +is taken in game. + +## What is the goal of this game when randomized? + +The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`. diff --git a/worlds/wargroove/docs/wargroove_en.md b/worlds/wargroove/docs/wargroove_en.md new file mode 100644 index 00000000..121e8c08 --- /dev/null +++ b/worlds/wargroove/docs/wargroove_en.md @@ -0,0 +1,83 @@ +# Wargroove Setup Guide + +## Required Files + +- Wargroove with the Double Trouble DLC installed through Steam on Windows + - Only the Steam Windows version is supported. MAC, Switch, Xbox, and Playstation are not supported. +- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Backup playerProgress files +`playerProgress` and `playerProgress.bak` contain save data for all of your Wargroove campaigns. Backing up these files +is strongly recommended in case they become corrupted. +1. Type `%appdata%\Chucklefish\Wargroove\save` in the file browser and hit enter. +2. Copy the `playerProgress` and `playerProgress.bak` files and paste them into a backup directory. + +## Update host.yaml to include the Wargroove root directory + +1. Look for your Archipelago install files. By default, the installer puts them in `C:\ProgramData\Archipelago`. +2. Open the `host.yaml` file in your favorite text editor (Notepad will work). +3. Put your Wargroove root directory in the `root_directory:` under the `wargroove_options:` section. + - The Wargroove root directory can be found by going to + `Steam->Right Click Wargroove->Properties->Local Files->Browse Local Files` and copying the path in the address bar. + - Paste the path in between the quotes next to `root_directory:` in the `host.yaml`. + - You may have to replace all single \\ with \\\\. +4. Start the Wargroove client. + +## Installing the Archipelago Wargroove Mod and Campaign files + +1. Shut down Wargroove if it is open. +2. Start the ArchipelagoWargrooveClient.exe from the Archipelago installation. +This should install the mod and campaign for you. +3. Start Wargroove. + +## Verify the campaign can be loaded + +1. Start Wargroove from Steam. +2. Go to `Story->Campaign->Custom->Archipelago` and click play. You should see the first level. + +## Starting a Multiworld game + +1. Start the Wargroove Client and connect to the server. Enter your username from your +[settings file.](/games/Wargroove/player-settings) +2. Start Wargroove and play the Archipelago campaign by going to `Story->Campaign->Custom->Archipelago`. + +## Ending a Multiworld game +It is strongly recommended that you delete your campaign progress after finishing a multiworld game. +This can be done by going to the level selection screen in the Archipelago campaign, hitting `ESC` and clicking the +`Delete Progress` button. The main menu should now be visible. + +## Updating to a new version of the Wargroove mod or downloading new campaign files +First, delete your campaign progress by going to the level selection screen in the Archipelago campaign, +hitting `ESC` and clicking the `Delete Progress` button. + +Follow the `Installing the Archipelago Wargroove Mod and Campaign files` steps again, but look for the latest version + to download. In addition, follow the steps outlined in `Wargroove crashes when trying to run the Archipelago campaign` +when attempting to update the campaign files and the mod. + +## Troubleshooting + +### The game is too hard +`Go to the campaign overview screen->Hit escape on the keyboard->Click adjust difficulty->Adjust the setttings` + +### The mod doesn't load +Double-check the mod installation under `%appdata%\Chucklefish\Wargroove\mods`. There should be 3 `.dat` files in +`%appdata%\Chucklefish\Wargroove\mods\ArchipelagoMod`. Otherwise, follow +`Installing the Archipelago Wargroove Mod and Campaign files` steps once more. + +### Wargroove crashes or there is a lua error +Wargroove is finicky, but there could be several causes for this. If it happens often or can be reproduced, +please submit a bug report in the tech-support channel on the [discord](https://discord.gg/archipelago). + +### Wargroove crashes when trying to run the Archipelago campaign +This is caused by not deleting campaign progress before updating the mod and campaign files. +1. Go to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod. +2. Wargroove will give an error message. +3. Go back to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod again. +4. Wargroove crashes. +5. Go back to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod again. +6. In the edit menu, hit `ESC` and click `Delete Progress`. +7. If the above steps do not allow you to start the campaign from `Story->Campaign->Custom->Archipelago` replace +`playerProgress` and `playerProgress.bak` with your previously backed up files. + +### Mod is out of date when trying to run the Archipelago campaign +Please follow the above steps in `Wargroove crashes when trying to run the Archipelago campaign`. \ No newline at end of file