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