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'),
|
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||||
# Starcraft 2
|
# Starcraft 2
|
||||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||||
|
# Wargroove
|
||||||
|
Component('Wargroove Client', 'WargrooveClient'),
|
||||||
# Zillion
|
# Zillion
|
||||||
Component('Zillion Client', 'ZillionClient',
|
Component('Zillion Client', 'ZillionClient',
|
||||||
file_identifier=SuffixIdentifier('.apzl')),
|
file_identifier=SuffixIdentifier('.apzl')),
|
||||||
|
|
3
Utils.py
3
Utils.py
|
@ -310,6 +310,9 @@ def get_default_options() -> OptionsType:
|
||||||
"lufia2ac_options": {
|
"lufia2ac_options": {
|
||||||
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
||||||
},
|
},
|
||||||
|
"wargroove_options": {
|
||||||
|
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return options
|
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
|
# True for operating system default program
|
||||||
# Alternatively, a path to a program to open the .gb file with
|
# Alternatively, a path to a program to open the .gb file with
|
||||||
rom_start: true
|
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:
|
zillion_options:
|
||||||
# File name of the Zillion US rom
|
# File name of the Zillion US rom
|
||||||
rom_file: "Zillion (UE) [!].sms"
|
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