Wargroove: Implement New Game (#1401)
This adds Wargroove to the list of supported games. Wargroove uses a custom non-linear campaign over the vanilla and double trouble campaigns. A Wargroove client has been added which does a lot of heavy lifting for the Wargroove implementation and must be always on during gameplay. The mod source files can be found here: https://github.com/FlySniper/WargrooveArchipelagoMod
This commit is contained in:
		
							parent
							
								
									7c68e91d4a
								
							
						
					
					
						commit
						5966aa5327
					
				|  | @ -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')), | ||||
|  |  | |||
							
								
								
									
										3
									
								
								Utils.py
								
								
								
								
							
							
						
						
									
										3
									
								
								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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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') | ||||
|     ] | ||||
| } | ||||
|  | @ -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, | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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)) | ||||
|  | @ -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 | ||||
|  | @ -0,0 +1,28 @@ | |||
| <FactionBox>: | ||||
|     orientation: 'vertical' | ||||
|     padding: [10,5,10,5] | ||||
|     size_hint_y: 0.14 | ||||
| 
 | ||||
| <CommanderGroup>: | ||||
|     orientation: 'horizontal' | ||||
| 
 | ||||
| <CommanderButton>: | ||||
|     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') | ||||
| 
 | ||||
| <ItemTracker>: | ||||
|     orientation: 'horizontal' | ||||
|     padding_y: 5 | ||||
| 
 | ||||
| <ItemLabel>: | ||||
|     size_hint_x: None | ||||
|     size: self.texture_size | ||||
|     pos_hint: {'left': 1} | ||||
|  | @ -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 | ||||
|         ) | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -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`. | ||||
|  | @ -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`. | ||||
		Loading…
	
		Reference in New Issue