Starcraft 2 Wings of Liberty AP Implementation (#528)
This commit is contained in:
parent
90d506ee7c
commit
551cf8442f
|
@ -152,6 +152,8 @@ components: Iterable[Component] = (
|
|||
Component('FF1 Client', 'FF1Client'),
|
||||
# ChecksFinder
|
||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
Component('Starcraft 2 Client', 'StarcraftClient'),
|
||||
# Functions
|
||||
Component('Open host.yaml', func=open_host_yaml),
|
||||
Component('Open Patch', func=open_patch),
|
||||
|
|
|
@ -25,6 +25,7 @@ Currently, the following games are supported:
|
|||
* Hollow Knight
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2: Wings of Liberty
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
|
@ -0,0 +1,520 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import multiprocessing
|
||||
import logging
|
||||
import asyncio
|
||||
import nest_asyncio
|
||||
|
||||
import sc2
|
||||
|
||||
from sc2.main import run_game
|
||||
from sc2.data import Race
|
||||
from sc2.bot_ai import BotAI
|
||||
from sc2.player import Bot
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table
|
||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||
|
||||
from Utils import init_logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging("SC2Client", exception_logger="Client")
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
sc2_logger = logging.getLogger("Starcraft2")
|
||||
|
||||
import colorama
|
||||
|
||||
from NetUtils import *
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser
|
||||
|
||||
nest_asyncio.apply()
|
||||
|
||||
class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
ctx: Context
|
||||
|
||||
def _cmd_play(self, mission_id: str = "") -> bool:
|
||||
"""Start a Starcraft 2 mission"""
|
||||
|
||||
options = mission_id.split()
|
||||
num_options = len(options)
|
||||
|
||||
if num_options > 0:
|
||||
mission_number = int(options[0])
|
||||
|
||||
if is_mission_available(mission_number, self.ctx.checked_locations, mission_req_table):
|
||||
asyncio.create_task(starcraft_launch(self.ctx, mission_number), name="Starcraft Launch")
|
||||
else:
|
||||
sc2_logger.info(
|
||||
"This mission is not currently unlocked. Use /unfinished or /available to see what is available.")
|
||||
|
||||
else:
|
||||
sc2_logger.info("Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
|
||||
|
||||
return True
|
||||
|
||||
def _cmd_available(self) -> bool:
|
||||
"""Get what missions are currently available to play"""
|
||||
|
||||
request_available_missions(self.ctx.checked_locations, mission_req_table)
|
||||
return True
|
||||
|
||||
def _cmd_unfinished(self) -> bool:
|
||||
"""Get what missions are currently available to play and have not had all locations checked"""
|
||||
|
||||
request_unfinished_missions(self.ctx.checked_locations, mission_req_table)
|
||||
return True
|
||||
|
||||
|
||||
class Context(CommonContext):
|
||||
command_processor = StarcraftClientProcessor
|
||||
game = "Starcraft 2 Wings of Liberty"
|
||||
items_handling = 0b111
|
||||
difficulty = -1
|
||||
all_in_choice = 0
|
||||
items_rec_to_announce = []
|
||||
rec_announce_pos = 0
|
||||
items_sent_to_announce = []
|
||||
sent_announce_pos = 0
|
||||
announcements = []
|
||||
announcement_pos = 0
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(Context, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"Connected"}:
|
||||
self.difficulty = args["slot_data"]["game_difficulty"]
|
||||
self.all_in_choice = args["slot_data"]["all_in_map"]
|
||||
if cmd in {"PrintJSON"}:
|
||||
noted = False
|
||||
if "receiving" in args:
|
||||
if args["receiving"] == self.slot:
|
||||
self.announcements.append(args["data"])
|
||||
noted = True
|
||||
if not noted and "item" in args:
|
||||
if args["item"].player == self.slot:
|
||||
self.announcements.append(args["data"])
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class SC2Manager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("Starcraft2", "Starcraft2"),
|
||||
]
|
||||
base_title = "Archipelago Starcraft 2 Client"
|
||||
|
||||
self.ui = SC2Manager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
async def main():
|
||||
multiprocessing.freeze_support()
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = Context(args.connect, args.password)
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
input_task = None
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
if sys.stdin:
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
ctx.snes_reconnect_address = None
|
||||
await ctx.shutdown()
|
||||
|
||||
if ui_task:
|
||||
await ui_task
|
||||
|
||||
if input_task:
|
||||
input_task.cancel()
|
||||
|
||||
maps_table = ["ap_traynor01", "ap_traynor02", "ap_traynor03", "ap_thanson01", "ap_thanson02", "ap_thanson03a", "ap_thanson03b", "ap_ttychus01",
|
||||
"ap_ttychus02", "ap_ttychus03", "ap_ttychus04", "ap_ttychus05", "ap_ttosh01", "ap_ttosh02", "ap_ttosh03a", "ap_ttosh03b",
|
||||
"ap_thorner01", "ap_thorner02", "ap_thorner03", "ap_thorner04", "ap_thorner05s", "ap_tzeratul01", "ap_tzeratul02",
|
||||
"ap_tzeratul03", "ap_tzeratul04", "ap_tvalerian01", "ap_tvalerian02a", "ap_tvalerian02b", "ap_tvalerian03"]
|
||||
|
||||
|
||||
def calculate_items(items):
|
||||
unit_unlocks = 0
|
||||
armory1_unlocks = 0
|
||||
armory2_unlocks = 0
|
||||
upgrade_unlocks = 0
|
||||
building_unlocks = 0
|
||||
merc_unlocks = 0
|
||||
lab_unlocks = 0
|
||||
protoss_unlock = 0
|
||||
minerals = 0
|
||||
vespene = 0
|
||||
|
||||
for item in items:
|
||||
data = lookup_id_to_name[item.item]
|
||||
|
||||
if item_table[data].type == "Unit":
|
||||
unit_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Upgrade":
|
||||
upgrade_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 1":
|
||||
armory1_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 2":
|
||||
armory2_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Building":
|
||||
building_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Mercenary":
|
||||
merc_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Laboratory":
|
||||
lab_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Protoss":
|
||||
protoss_unlock += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Minerals":
|
||||
minerals += item_table[data].number
|
||||
elif item_table[data].type == "Vespene":
|
||||
vespene += item_table[data].number
|
||||
|
||||
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
|
||||
lab_unlocks, protoss_unlock, minerals, vespene]
|
||||
|
||||
|
||||
def calc_difficulty(difficulty):
|
||||
if difficulty == 0:
|
||||
return 'C'
|
||||
elif difficulty == 1:
|
||||
return 'N'
|
||||
elif difficulty == 2:
|
||||
return 'H'
|
||||
elif difficulty == 3:
|
||||
return 'B'
|
||||
|
||||
return 'X'
|
||||
|
||||
|
||||
async def starcraft_launch(ctx: Context, mission_id):
|
||||
ctx.rec_announce_pos = len(ctx.items_rec_to_announce)
|
||||
ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
|
||||
ctx.announcements_pos = len(ctx.announcements)
|
||||
|
||||
run_game(sc2.maps.get(maps_table[mission_id-1]), [
|
||||
Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), name="Archipelago")], realtime=True)
|
||||
|
||||
|
||||
class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
game_running = False
|
||||
mission_completed = False
|
||||
first_bonus = False
|
||||
second_bonus = False
|
||||
third_bonus = False
|
||||
fourth_bonus = False
|
||||
fifth_bonus = False
|
||||
sixth_bonus = False
|
||||
seventh_bonus = False
|
||||
eight_bonus = False
|
||||
ctx: Context = None
|
||||
mission_id = 0
|
||||
|
||||
can_read_game = False
|
||||
|
||||
last_received_update = 0
|
||||
|
||||
def __init__(self, ctx: Context, mission_id):
|
||||
self.ctx = ctx
|
||||
self.mission_id = mission_id
|
||||
|
||||
super(ArchipelagoBot, self).__init__()
|
||||
|
||||
async def on_step(self, iteration: int):
|
||||
game_state = 0
|
||||
if iteration == 0:
|
||||
start_items = calculate_items(self.ctx.items_received)
|
||||
difficulty = calc_difficulty(self.ctx.difficulty)
|
||||
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {}".format(
|
||||
difficulty, start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], start_items[5],
|
||||
start_items[6], start_items[7], start_items[8], start_items[9], self.ctx.all_in_choice))
|
||||
self.last_received_update = len(self.ctx.items_received)
|
||||
|
||||
else:
|
||||
if self.ctx.announcement_pos < len(self.ctx.announcements):
|
||||
index = 0
|
||||
message = ""
|
||||
while index < len(self.ctx.announcements[self.ctx.announcement_pos]):
|
||||
message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"]
|
||||
index += 1
|
||||
|
||||
index = 0
|
||||
start_rem_pos = -1
|
||||
# Remove unneeded [Color] tags
|
||||
while index < len(message):
|
||||
if message[index] == '[':
|
||||
start_rem_pos = index
|
||||
index += 1
|
||||
elif message[index] == ']' and start_rem_pos > -1:
|
||||
temp_msg = ""
|
||||
|
||||
if start_rem_pos > 0:
|
||||
temp_msg = message[:start_rem_pos]
|
||||
if index < len(message) - 1:
|
||||
temp_msg += message[index+1:]
|
||||
|
||||
message = temp_msg
|
||||
index += start_rem_pos - index
|
||||
start_rem_pos = -1
|
||||
else:
|
||||
index += 1
|
||||
|
||||
await self.chat_send("SendMessage " + message)
|
||||
self.ctx.announcement_pos += 1
|
||||
|
||||
# Archipelago reads the health
|
||||
for unit in self.all_own_units():
|
||||
if unit.health_max == 38281:
|
||||
game_state = int(38281 - unit.health)
|
||||
can_read_game = True
|
||||
|
||||
if iteration == 80 and not game_state & 1:
|
||||
await self.chat_send("SendMessage Warning: Archipelago unable to connect or has lost connection to " +
|
||||
"Starcraft 2 (This is likely a map issue)")
|
||||
|
||||
if self.last_received_update < len(self.ctx.items_received):
|
||||
current_items = calculate_items(self.ctx.items_received)
|
||||
await self.chat_send("UpdateTech {} {} {} {} {} {} {} {}".format(
|
||||
current_items[0], current_items[1], current_items[2], current_items[3], current_items[4], current_items[5],
|
||||
current_items[6], current_items[7]))
|
||||
self.last_received_update = len(self.ctx.items_received)
|
||||
|
||||
if game_state & 1:
|
||||
if not self.game_running:
|
||||
print("Archipelago Connected")
|
||||
self.game_running = True
|
||||
|
||||
if can_read_game:
|
||||
if game_state & (1 << 1) and not self.mission_completed:
|
||||
if self.mission_id != 29:
|
||||
print("Mission Completed")
|
||||
await self.ctx.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}])
|
||||
self.mission_completed = True
|
||||
else:
|
||||
print("Game Complete")
|
||||
await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
|
||||
self.mission_completed = True
|
||||
|
||||
if game_state & (1 << 2) and not self.first_bonus:
|
||||
print("1st Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}])
|
||||
self.first_bonus = True
|
||||
|
||||
if not self.second_bonus and game_state & (1 << 3):
|
||||
print("2nd Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}])
|
||||
self.second_bonus = True
|
||||
|
||||
if not self.third_bonus and game_state & (1 << 4):
|
||||
print("3rd Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}])
|
||||
self.third_bonus = True
|
||||
|
||||
if not self.fourth_bonus and game_state & (1 << 5):
|
||||
print("4th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}])
|
||||
self.fourth_bonus = True
|
||||
|
||||
if not self.fifth_bonus and game_state & (1 << 6):
|
||||
print("5th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}])
|
||||
self.fifth_bonus = True
|
||||
|
||||
if not self.sixth_bonus and game_state & (1 << 7):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}])
|
||||
self.sixth_bonus = True
|
||||
|
||||
if not self.seventh_bonus and game_state & (1 << 8):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}])
|
||||
self.seventh_bonus = True
|
||||
|
||||
if not self.eight_bonus and game_state & (1 << 9):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}])
|
||||
self.eight_bonus = True
|
||||
|
||||
else:
|
||||
await self.chat_send("LostConnection - Lost connection to game.")
|
||||
|
||||
|
||||
class MissionInfo(typing.NamedTuple):
|
||||
id: int
|
||||
extra_locations: int
|
||||
required_world: list[int]
|
||||
number: int = 0 # number of worlds need beaten
|
||||
|
||||
|
||||
mission_req_table = {
|
||||
"Liberation Day": MissionInfo(1, 7, []),
|
||||
"The Outlaws": MissionInfo(2, 2, [1]),
|
||||
"Zero Hour": MissionInfo(3, 4, [2]),
|
||||
"Evacuation": MissionInfo(4, 4, [3]),
|
||||
"Outbreak": MissionInfo(5, 3, [4]),
|
||||
"Safe Haven": MissionInfo(6, 1, [5], number=7),
|
||||
"Haven's Fall": MissionInfo(7, 1, [5], number=7),
|
||||
"Smash and Grab": MissionInfo(8, 5, [3]),
|
||||
"The Dig": MissionInfo(9, 4, [8], number=8),
|
||||
"The Moebius Factor": MissionInfo(10, 9, [9], number=11),
|
||||
"Supernova": MissionInfo(11, 5, [10], number=14),
|
||||
"Maw of the Void": MissionInfo(12, 6, [11]),
|
||||
"Devil's Playground": MissionInfo(13, 3, [3], number=4),
|
||||
"Welcome to the Jungle": MissionInfo(14, 4, [13]),
|
||||
"Breakout": MissionInfo(15, 3, [14], number=8),
|
||||
"Ghost of a Chance": MissionInfo(16, 6, [14], number=8),
|
||||
"The Great Train Robbery": MissionInfo(17, 4, [3], number=6),
|
||||
"Cutthroat": MissionInfo(18, 5, [17]),
|
||||
"Engine of Destruction": MissionInfo(19, 6, [18]),
|
||||
"Media Blitz": MissionInfo(20, 5, [19]),
|
||||
"Piercing the Shroud": MissionInfo(21, 6, [20]),
|
||||
"Whispers of Doom": MissionInfo(22, 4, [9]),
|
||||
"A Sinister Turn": MissionInfo(23, 4, [22]),
|
||||
"Echoes of the Future": MissionInfo(24, 3, [23]),
|
||||
"In Utter Darkness": MissionInfo(25, 3, [24]),
|
||||
"Gates of Hell": MissionInfo(26, 2, [12]),
|
||||
"Belly of the Beast": MissionInfo(27, 4, [26]),
|
||||
"Shatter the Sky": MissionInfo(28, 5, [26]),
|
||||
"All-In": MissionInfo(29, -1, [27, 28])
|
||||
}
|
||||
|
||||
|
||||
def calc_objectives_completed(mission, missions_info, locations_done):
|
||||
objectives_complete = 0
|
||||
|
||||
if missions_info[mission].extra_locations > 0:
|
||||
for i in range(missions_info[mission].extra_locations):
|
||||
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
|
||||
objectives_complete += 1
|
||||
|
||||
return objectives_complete
|
||||
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def request_unfinished_missions(locations_done, location_table):
|
||||
message = "Unfinished Missions:"
|
||||
|
||||
first_item = True
|
||||
|
||||
unfinished_missions = calc_unfinished_missions(locations_done, location_table)
|
||||
|
||||
for mission in unfinished_missions:
|
||||
if first_item:
|
||||
message += " {}[{}] ({}/{})".format(mission, location_table[mission].id, unfinished_missions[mission],
|
||||
location_table[mission].extra_locations)
|
||||
first_item = False
|
||||
else:
|
||||
message += ", {}[{}] ({}/{})".format(mission, location_table[mission].id, unfinished_missions[mission],
|
||||
location_table[mission].extra_locations)
|
||||
|
||||
sc2_logger.info(message)
|
||||
|
||||
|
||||
def calc_unfinished_missions(locations_done, locations):
|
||||
unfinished_missions = []
|
||||
locations_completed = []
|
||||
available_missions = calc_available_missions(locations_done, locations)
|
||||
|
||||
for name in available_missions:
|
||||
if not locations[name].extra_locations == -1:
|
||||
objectives_completed = calc_objectives_completed(name, locations, locations_done)
|
||||
|
||||
if objectives_completed < locations[name].extra_locations:
|
||||
unfinished_missions.append(name)
|
||||
locations_completed.append(objectives_completed)
|
||||
|
||||
else:
|
||||
unfinished_missions.append(name)
|
||||
locations_completed.append(-1)
|
||||
|
||||
return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))}
|
||||
|
||||
|
||||
def is_mission_available(mission_id_to_check, locations_done, locations):
|
||||
unfinished_missions = calc_available_missions(locations_done, locations)
|
||||
|
||||
for mission in unfinished_missions:
|
||||
if locations[mission].id == mission_id_to_check:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def request_available_missions(locations_done, location_table):
|
||||
message = "Available Missions:"
|
||||
|
||||
first_item = True
|
||||
|
||||
missions = calc_available_missions(locations_done, location_table)
|
||||
|
||||
for mission in missions:
|
||||
if first_item:
|
||||
message += " {}[{}]".format(mission, location_table[mission].id)
|
||||
first_item = False
|
||||
else:
|
||||
message += ", {}[{}]".format(mission, location_table[mission].id)
|
||||
|
||||
sc2_logger.info(message)
|
||||
|
||||
|
||||
def calc_available_missions(locations_done, locations):
|
||||
available_missions = []
|
||||
mission_complete = 0
|
||||
|
||||
# Get number of missions completed
|
||||
for loc in locations_done:
|
||||
if loc % 100 == 0:
|
||||
mission_complete += 1
|
||||
|
||||
for name in locations:
|
||||
if len(locations[name].required_world) >= 1:
|
||||
reqs_complete = True
|
||||
|
||||
for req_mission in locations[name].required_world:
|
||||
if not(req_mission * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done:
|
||||
reqs_complete = False
|
||||
break
|
||||
|
||||
if reqs_complete and mission_complete >= locations[name].number:
|
||||
available_missions.append(name)
|
||||
else:
|
||||
available_missions.append(name)
|
||||
|
||||
return available_missions
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
loop.close()
|
||||
colorama.deinit()
|
|
@ -67,6 +67,7 @@ Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDi
|
|||
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
||||
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
|
||||
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
||||
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
|
@ -92,6 +93,7 @@ Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: igno
|
|||
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
|
||||
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
|
||||
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
|
@ -104,6 +106,7 @@ Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinec
|
|||
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
|
||||
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
|
||||
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
|
||||
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
|
||||
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||
|
@ -113,6 +116,7 @@ Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\Archipel
|
|||
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
|
||||
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
|
||||
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
|
||||
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
|
||||
|
||||
[Run]
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -348,7 +348,7 @@ cx_Freeze.setup(
|
|||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "kivy"],
|
||||
"zip_exclude_packages": ["worlds", "kivy", "sc2"],
|
||||
"include_files": [],
|
||||
"include_msvcr": False,
|
||||
"replace_paths": [("*", "")],
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
from BaseClasses import Item
|
||||
import typing
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: typing.Optional[int]
|
||||
type: typing.Optional[str]
|
||||
number: typing.Optional[int]
|
||||
progression: bool = False
|
||||
never_exclude: bool = True
|
||||
quantity: int = 1
|
||||
|
||||
|
||||
class StarcraftWoLItem(Item):
|
||||
game: str = "Starcraft2WoL"
|
||||
|
||||
def __init__(self, name, advancement: bool = False, code: int = None, player: int = None):
|
||||
super(StarcraftWoLItem, self).__init__(name, advancement, code, player)
|
||||
|
||||
|
||||
def get_full_item_list():
|
||||
return item_table
|
||||
|
||||
|
||||
SC2WOL_ITEM_ID_OFFSET = 1000
|
||||
|
||||
item_table = {
|
||||
"Marine": ItemData(0+SC2WOL_ITEM_ID_OFFSET, "Unit", 0, progression=True),
|
||||
"Medic": ItemData(1+SC2WOL_ITEM_ID_OFFSET, "Unit", 1, progression=True),
|
||||
"Firebat": ItemData(2+SC2WOL_ITEM_ID_OFFSET, "Unit", 2, progression=True),
|
||||
"Marauder": ItemData(3+SC2WOL_ITEM_ID_OFFSET, "Unit", 3, progression=True),
|
||||
"Reaper": ItemData(4+SC2WOL_ITEM_ID_OFFSET, "Unit", 4, progression=True),
|
||||
"Hellion": ItemData(5+SC2WOL_ITEM_ID_OFFSET, "Unit", 5, progression=True),
|
||||
"Vulture": ItemData(6+SC2WOL_ITEM_ID_OFFSET, "Unit", 6, progression=True),
|
||||
"Goliath": ItemData(7+SC2WOL_ITEM_ID_OFFSET, "Unit", 7, progression=True),
|
||||
"Diamondback": ItemData(8+SC2WOL_ITEM_ID_OFFSET, "Unit", 8, progression=True),
|
||||
"Siege Tank": ItemData(9+SC2WOL_ITEM_ID_OFFSET, "Unit", 9, progression=True),
|
||||
"Medivac": ItemData(10+SC2WOL_ITEM_ID_OFFSET, "Unit", 10, progression=True),
|
||||
"Wraith": ItemData(11+SC2WOL_ITEM_ID_OFFSET, "Unit", 11, progression=True),
|
||||
"Viking": ItemData(12+SC2WOL_ITEM_ID_OFFSET, "Unit", 12, progression=True),
|
||||
"Banshee": ItemData(13+SC2WOL_ITEM_ID_OFFSET, "Unit", 13, progression=True),
|
||||
"Battlecruiser": ItemData(14+SC2WOL_ITEM_ID_OFFSET, "Unit", 14, progression=True),
|
||||
"Ghost": ItemData(15+SC2WOL_ITEM_ID_OFFSET, "Unit", 15, progression=True),
|
||||
"Spectre": ItemData(16+SC2WOL_ITEM_ID_OFFSET, "Unit", 16, progression=True),
|
||||
"Thor": ItemData(17+SC2WOL_ITEM_ID_OFFSET, "Unit", 17, progression=True),
|
||||
|
||||
"Progressive Infantry Weapon": ItemData (100+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
|
||||
"Progressive Infantry Armor": ItemData (102+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
|
||||
"Progressive Vehicle Weapon": ItemData (103+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
|
||||
"Progressive Vehicle Armor": ItemData (104+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, quantity=3),
|
||||
"Progressive Ship Weapon": ItemData (105+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
|
||||
"Progressive Ship Armor": ItemData (106+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
|
||||
|
||||
"Projectile Accelerator (Bunker)": ItemData (200+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0),
|
||||
"Neosteel Bunker (Bunker)": ItemData (201+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1),
|
||||
"Titanium Housing (Missile Turret)": ItemData (202+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2),
|
||||
"Hellstorm Batteries (Missile Turret)": ItemData (203+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3),
|
||||
"Advanced Construction (SCV)": ItemData (204+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
|
||||
"Dual-Fusion Welders (SCV)": ItemData (205+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
|
||||
"Fire-Suppression System (Building)": ItemData (206+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6),
|
||||
"Orbital Command (Building)": ItemData (207+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
|
||||
"Stimpack (Marine)": ItemData (208+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8),
|
||||
"Combat Shield (Marine)": ItemData (209+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9),
|
||||
"Advanced Medic Facilities (Medic)": ItemData (210+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10),
|
||||
"Stabilizer Medpacks (Medic)": ItemData (211+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData (212+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12),
|
||||
"Juggernaut Plating (Firebat)": ItemData (213+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13),
|
||||
"Concussive Shells (Marauder)": ItemData (214+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14),
|
||||
"Kinetic Foam (Marauder)": ItemData (215+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15),
|
||||
"U-238 Rounds (Reaper)": ItemData (216+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData (217+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17),
|
||||
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300+SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0),
|
||||
"Thermite Filaments (Hellion)": ItemData(301+SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1),
|
||||
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2),
|
||||
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3),
|
||||
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4),
|
||||
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7),
|
||||
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8),
|
||||
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10),
|
||||
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12),
|
||||
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13),
|
||||
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14),
|
||||
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16),
|
||||
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19),
|
||||
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20),
|
||||
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21),
|
||||
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22),
|
||||
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25),
|
||||
|
||||
"Bunker": ItemData (400+SC2WOL_ITEM_ID_OFFSET, "Building", 0, progression=True),
|
||||
"Missile Turret": ItemData (401+SC2WOL_ITEM_ID_OFFSET, "Building", 1, progression=True),
|
||||
"Sensor Tower": ItemData (402+SC2WOL_ITEM_ID_OFFSET, "Building", 2),
|
||||
|
||||
"War Pigs": ItemData (500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0),
|
||||
"Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1),
|
||||
"Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2),
|
||||
"Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3),
|
||||
"Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4),
|
||||
"Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5),
|
||||
"Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6),
|
||||
"Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7),
|
||||
|
||||
"Ultra-Capacitors": ItemData(600 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 0),
|
||||
"Vanadium Plating": ItemData(601 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 1),
|
||||
"Orbital Depots": ItemData(602 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 2),
|
||||
"Micro-Filtering": ItemData(603 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 3),
|
||||
"Automated Refinery": ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4),
|
||||
"Command Center Reactor": ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5),
|
||||
"Raven": ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6),
|
||||
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7),
|
||||
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
|
||||
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
|
||||
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10),
|
||||
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11),
|
||||
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12),
|
||||
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13),
|
||||
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14),
|
||||
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, progression=True),
|
||||
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16),
|
||||
"Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17),
|
||||
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19),
|
||||
|
||||
"Zealot": ItemData (700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, progression=True),
|
||||
"Stalker": ItemData (701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, progression=True),
|
||||
"High Templar": ItemData (702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, progression=True),
|
||||
"Dark Templar": ItemData (703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, progression=True),
|
||||
"Immortal": ItemData (704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, progression=True),
|
||||
"Colossus": ItemData (705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5, progression=True),
|
||||
"Phoenix": ItemData (706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, progression=True),
|
||||
"Void Ray": ItemData (707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, progression=True),
|
||||
"Carrier": ItemData (708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, progression=True),
|
||||
|
||||
"+5 Starting Minerals": ItemData(800+SC2WOL_ITEM_ID_OFFSET, "Minerals", 5, quantity=0),
|
||||
"+5 Starting Vespene": ItemData(801+SC2WOL_ITEM_ID_OFFSET, "Vespene", 5, quantity=0)
|
||||
}
|
||||
|
||||
basic_unit: typing.Tuple[str, ...] = (
|
||||
'Marine',
|
||||
'Marauder',
|
||||
'Firebat',
|
||||
'Hellion',
|
||||
'Vulture'
|
||||
)
|
||||
|
||||
|
||||
item_name_groups = {"Missions":
|
||||
{"Beat Liberation Day", "Beat The Outlaws", "Beat Zero Hour", "Beat Evacuation",
|
||||
"None Outbreak", "Beat Safe Haven", "Beat Haven's Fall", "Beat Smash and Grab", "Beat The Dig",
|
||||
"Beat The Moebius Factor", "Beat Supernova", "Beat Maw of the Void", "Beat Devil's Playground",
|
||||
"Beat Welcome to the Jungle", "Beat Breakout", "Beat Ghost of a Chance",
|
||||
"Beat The Great Train Robbery", "Beat Cutthroat", "Beat Engine of Destruction",
|
||||
"Beat Media Blitz", "Beat Piercing the Shroud"}}
|
||||
|
||||
filler_items: typing.Tuple[str, ...] = (
|
||||
'+5 Starting Minerals',
|
||||
'+5 Starting Vespene'
|
||||
)
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if data.code}
|
|
@ -0,0 +1,186 @@
|
|||
from typing import List, Tuple, Optional, Callable, NamedTuple
|
||||
from BaseClasses import MultiWorld
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
SC2WOL_LOC_ID_OFFSET = 1000
|
||||
|
||||
class SC2WoLLocation(Location):
|
||||
game: str = "Starcraft2WoL"
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
region: str
|
||||
name: str
|
||||
code: Optional[int]
|
||||
rule: Callable = lambda state: True
|
||||
|
||||
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||
location_table: List[LocationData] = [
|
||||
LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100),
|
||||
LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101),
|
||||
LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102),
|
||||
LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103),
|
||||
LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104),
|
||||
LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105),
|
||||
LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106),
|
||||
LocationData("Liberation Day", "Beat Liberation Day", None),
|
||||
LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200),
|
||||
LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201),
|
||||
LocationData("The Outlaws", "Beat The Outlaws", None),
|
||||
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300),
|
||||
LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301),
|
||||
LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302),
|
||||
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303),
|
||||
LocationData("Zero Hour", "Beat Zero Hour", None),
|
||||
LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400),
|
||||
LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
|
||||
LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402),
|
||||
LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403),
|
||||
LocationData("Evacuation", "Beat Evacuation", None),
|
||||
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500),
|
||||
LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501),
|
||||
LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502),
|
||||
LocationData("Outbreak", "Beat Outbreak", None),
|
||||
LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600),
|
||||
LocationData("Safe Haven", "Beat Safe Haven", None),
|
||||
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700),
|
||||
LocationData("Haven's Fall", "Beat Haven's Fall", None),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800),
|
||||
LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804),
|
||||
LocationData("Smash and Grab", "Beat Smash and Grab", None),
|
||||
LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900),
|
||||
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901),
|
||||
LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902),
|
||||
LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903),
|
||||
LocationData("The Dig", "Beat The Dig", None),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1000),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008),
|
||||
LocationData("The Moebius Factor", "Beat The Moebius Factor", None),
|
||||
LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100),
|
||||
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
|
||||
LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102),
|
||||
LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103),
|
||||
LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104),
|
||||
LocationData("Supernova", "Beat Supernova", None),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Xel'Naga Vault", SC2WOL_LOC_ID_OFFSET + 1200),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202),
|
||||
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203),
|
||||
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204),
|
||||
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205),
|
||||
LocationData("Maw of the Void", "Beat Maw of the Void", None),
|
||||
LocationData("Devil's Playground", "Devil's Playground: 8000 Minerals", SC2WOL_LOC_ID_OFFSET + 1300),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302),
|
||||
LocationData("Devil's Playground", "Beat Devil's Playground", None),
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: 7 Canisters", SC2WOL_LOC_ID_OFFSET + 1400),
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401),
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402),
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403),
|
||||
LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None),
|
||||
LocationData("Breakout", "Breakout: Main Prison", SC2WOL_LOC_ID_OFFSET + 1500),
|
||||
LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501),
|
||||
LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502),
|
||||
LocationData("Breakout", "Beat Breakout", None),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Psi-Indoctrinator", SC2WOL_LOC_ID_OFFSET + 1600),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605),
|
||||
LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: 8 Trains", SC2WOL_LOC_ID_OFFSET + 1700, lambda state: state._sc2wol_has_train_killers(world, player)),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
|
||||
LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None,
|
||||
lambda state: state._sc2wol_has_train_killers(world, player)),
|
||||
LocationData("Cutthroat", "Cutthroat: Orlan's Planetary", SC2WOL_LOC_ID_OFFSET + 1800),
|
||||
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801),
|
||||
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802),
|
||||
LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803),
|
||||
LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804),
|
||||
LocationData("Cutthroat", "Beat Cutthroat", None),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Dominion Bases", SC2WOL_LOC_ID_OFFSET + 1900,
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player)),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902,
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player)),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904,
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player)),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905,
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player)),
|
||||
LocationData("Engine of Destruction", "Beat Engine of Destruction", None,
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player)),
|
||||
LocationData("Media Blitz", "Media Blitz: Full Upload", SC2WOL_LOC_ID_OFFSET + 2000),
|
||||
LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001),
|
||||
LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002),
|
||||
LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003),
|
||||
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
|
||||
LocationData("Media Blitz", "Beat Media Blitz", None),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Facility Escape", SC2WOL_LOC_ID_OFFSET + 2100),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105),
|
||||
LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Void Seeker Escape", SC2WOL_LOC_ID_OFFSET + 2200),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
|
||||
LocationData("Whispers of Doom", "Beat Whispers of Doom", None),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Preservers Freed", SC2WOL_LOC_ID_OFFSET + 2300,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("A Sinister Turn", "Beat A Sinister Turn", None,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: Overmind", SC2WOL_LOC_ID_OFFSET + 2400),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402),
|
||||
LocationData("Echoes of the Future", "Beat Echoes of the Future", None),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2500),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2502),
|
||||
LocationData("In Utter Darkness", "Beat In Utter Darkness", None),
|
||||
LocationData("Gates of Hell", "Gates of Hell: Nydus Worms", SC2WOL_LOC_ID_OFFSET + 2600),
|
||||
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601),
|
||||
LocationData("Gates of Hell", "Beat Gates of Hell", None),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Extract", SC2WOL_LOC_ID_OFFSET + 2700),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703),
|
||||
LocationData("Belly of the Beast", "Beat Belly of the Beast", None),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Platform Destroyed", SC2WOL_LOC_ID_OFFSET + 2800),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805),
|
||||
LocationData("Shatter the Sky", "Beat Shatter the Sky", None),
|
||||
LocationData("All-In", "All-In: Victory", None)
|
||||
]
|
||||
|
||||
return tuple(location_table)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class SC2WoLLogic(LogicMixin):
|
||||
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player)
|
||||
|
||||
def _sc2wol_has_bunker_unit(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Marauder'}, player)
|
||||
|
||||
def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Viking', 'Wraith', 'Medivac', 'Banshee', 'Hercules'}, player)
|
||||
|
||||
def _sc2wol_has_battlecruiser(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has('Battlecruiser', player)
|
||||
|
||||
def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Viking', 'Wraith'}, player)
|
||||
|
||||
def _sc2wol_has_mobile_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has('Missile Turret', player) or self._sc2wol_has_mobile_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has_any({'Siege Tank', 'Vulture'}, player) or
|
||||
self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \
|
||||
self._sc2wol_has_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has_any({'Siege Tank', 'Diamondback'}, player) or
|
||||
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
|
||||
|
||||
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player)
|
||||
|
||||
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
|
||||
|
||||
def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self._sc2wol_has_protoss_common_units(world, player) and \
|
||||
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player)
|
||||
|
||||
def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool:
|
||||
return self.has_group("Missions", player, mission_count)
|
||||
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
from typing import Dict
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Option
|
||||
|
||||
class GameDifficulty(Choice):
|
||||
"""The difficulty of the campaign, affects enemy AI, starting units, and game speed."""
|
||||
display_name = "Game Difficulty"
|
||||
option_casual = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
option_brutal = 3
|
||||
|
||||
class UpgradeBonus(Choice):
|
||||
"""Determines what lab upgrade to use, whether it is Ultra-Capacitors which boost attack speed with every weapon upgrade
|
||||
or Vanadium Plating which boosts life with every armor upgrade."""
|
||||
display_name = "Upgrade Bonus"
|
||||
option_ultra_capacitors = 0
|
||||
option_vanadium_plating = 1
|
||||
|
||||
class BunkerUpgrade(Choice):
|
||||
"""Determines what bunker lab upgrade to use, whether it is Shrike Turret which outfits bunkers with an automated turret or
|
||||
Fortified Bunker which boosts the life of bunkers."""
|
||||
display_name = "Bunker Upgrade"
|
||||
option_shrike_turret = 0
|
||||
option_fortified_bunker = 1
|
||||
|
||||
class AllInMap(Choice):
|
||||
"""Determines what verion of All-In (final map) that will be generated for the campaign."""
|
||||
display_name = "All In Map"
|
||||
option_ground = 0
|
||||
option_air = 1
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
sc2wol_options: Dict[str, Option] = {
|
||||
"game_difficulty": GameDifficulty,
|
||||
"upgrade_bonus": UpgradeBonus,
|
||||
"bunker_upgrade": BunkerUpgrade,
|
||||
"all_in_map": AllInMap,
|
||||
}
|
||||
|
||||
def get_option_value(world: MultiWorld, player: int, name: str) -> int:
|
||||
option = getattr(world, name, None)
|
||||
|
||||
if option == None:
|
||||
return 0
|
||||
|
||||
return int(option[player].value)
|
|
@ -0,0 +1,162 @@
|
|||
from typing import List, Set, Dict, Tuple, Optional, Callable
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
||||
from .Locations import LocationData
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]):
|
||||
locations_per_region = get_locations_per_region(locations)
|
||||
|
||||
regions = [
|
||||
create_region(world, player, locations_per_region, location_cache, "Menu"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Liberation Day"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Outlaws"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Zero Hour"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Evacuation"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Outbreak"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Safe Haven"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Haven's Fall"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Smash and Grab"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Dig"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Supernova"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Maw of the Void"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Devil's Playground"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Breakout"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Cutthroat"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Media Blitz"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Piercing the Shroud"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Whispers of Doom"),
|
||||
create_region(world, player, locations_per_region, location_cache, "A Sinister Turn"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Echoes of the Future"),
|
||||
create_region(world, player, locations_per_region, location_cache, "In Utter Darkness"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Gates of Hell"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Belly of the Beast"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Shatter the Sky"),
|
||||
create_region(world, player, locations_per_region, location_cache, "All-In")
|
||||
]
|
||||
|
||||
if __debug__:
|
||||
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
||||
|
||||
world.regions += regions
|
||||
|
||||
names: Dict[str, int] = {}
|
||||
|
||||
connect(world, player, names, 'Menu', 'Liberation Day'),
|
||||
connect(world, player, names, 'Liberation Day', 'The Outlaws',
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
connect(world, player, names, 'The Outlaws', 'Zero Hour'),
|
||||
connect(world, player, names, 'Zero Hour', 'Evacuation',
|
||||
lambda state: state._sc2wol_has_anti_air(world, player)),
|
||||
connect(world, player, names, 'Evacuation', 'Outbreak'),
|
||||
connect(world, player, names, "Outbreak", "Safe Haven",
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player) and
|
||||
state._sc2wol_cleared_missions(world, player, 7)),
|
||||
connect(world, player, names, "Outbreak", "Haven's Fall",
|
||||
lambda state: state._sc2wol_has_mobile_anti_air(world, player) and
|
||||
state._sc2wol_cleared_missions(world, player, 7)),
|
||||
connect(world, player, names, 'Zero Hour', 'Smash and Grab',
|
||||
lambda state: state._sc2wol_has_anti_air(world, player)),
|
||||
connect(world, player, names, 'Smash and Grab', 'The Dig',
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
|
||||
state._sc2wol_has_heavy_defense(world, player)),
|
||||
connect(world, player, names, 'The Dig', 'The Moebius Factor',
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 11) and
|
||||
state._sc2wol_has_air(world, player)),
|
||||
connect(world, player, names, 'The Moebius Factor', 'Supernova',
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 14)),
|
||||
connect(world, player, names, 'Supernova', 'Maw of the Void'),
|
||||
connect(world, player, names, 'Zero Hour', "Devil's Playground",
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 4)),
|
||||
connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle'),
|
||||
connect(world, player, names, "Welcome to the Jungle", 'Breakout',
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 8)),
|
||||
connect(world, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 8)),
|
||||
connect(world, player, names, "Zero Hour", 'The Great Train Robbery',
|
||||
lambda state: state._sc2wol_cleared_missions(world, player, 6)),
|
||||
connect(world, player, names, 'The Great Train Robbery', 'Cutthroat',
|
||||
lambda state: state.has("Beat The Great Train Robbery", player)),
|
||||
connect(world, player, names, 'Cutthroat', 'Engine of Destruction',
|
||||
lambda state: state.has("Beat The Great Train Robbery", player)),
|
||||
connect(world, player, names, 'Engine of Destruction', 'Media Blitz',
|
||||
lambda state: state.has("Beat Engine of Destruction", player)),
|
||||
connect(world, player, names, 'Media Blitz', 'Piercing the Shroud'),
|
||||
connect(world, player, names, 'The Dig', 'Whispers of Doom',),
|
||||
connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn'),
|
||||
connect(world, player, names, 'A Sinister Turn', 'Echoes of the Future',
|
||||
lambda state: state.has("Beat A Sinister Turn", player)),
|
||||
connect(world, player, names, 'Echoes of the Future', 'In Utter Darkness'),
|
||||
connect(world, player, names, 'Maw of the Void', 'Gates of Hell'),
|
||||
connect(world, player, names, 'Gates of Hell', 'Belly of the Beast'),
|
||||
connect(world, player, names, 'Gates of Hell', 'Shatter the Sky'),
|
||||
connect(world, player, names, 'Gates of Hell', 'All-In',
|
||||
lambda state: state.has('Beat Gates of Hell', player) or state.has('Beat Shatter the Sky', player))
|
||||
|
||||
|
||||
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):
|
||||
existingRegions = set()
|
||||
|
||||
for region in regions:
|
||||
existingRegions.add(region.name)
|
||||
|
||||
if (regionNames - existingRegions):
|
||||
raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions))
|
||||
|
||||
|
||||
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
|
||||
location = Location(player, location_data.name, location_data.code, region)
|
||||
location.access_rule = location_data.rule
|
||||
|
||||
if id is None:
|
||||
location.event = True
|
||||
location.locked = True
|
||||
|
||||
location_cache.append(location)
|
||||
|
||||
return location
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region:
|
||||
region = Region(name, RegionType.Generic, name, player)
|
||||
region.world = world
|
||||
|
||||
if name in locations_per_region:
|
||||
for location_data in locations_per_region[name]:
|
||||
location = create_location(player, location_data, region, location_cache)
|
||||
region.locations.append(location)
|
||||
|
||||
return region
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, rule: Optional[Callable] = None):
|
||||
sourceRegion = world.get_region(source, player)
|
||||
targetRegion = world.get_region(target, player)
|
||||
|
||||
if target not in used_names:
|
||||
used_names[target] = 1
|
||||
name = target
|
||||
else:
|
||||
used_names[target] += 1
|
||||
name = target + (' ' * used_names[target])
|
||||
|
||||
connection = Entrance(player, name, sourceRegion)
|
||||
|
||||
if rule:
|
||||
connection.access_rule = rule
|
||||
|
||||
sourceRegion.exits.append(connection)
|
||||
connection.connect(targetRegion)
|
||||
|
||||
|
||||
def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
|
||||
per_region: Dict[str, List[LocationData]] = {}
|
||||
|
||||
for location in locations:
|
||||
per_region.setdefault(location.region, []).append(location)
|
||||
|
||||
return per_region
|
|
@ -0,0 +1,173 @@
|
|||
import typing
|
||||
|
||||
from typing import List, Set, Tuple
|
||||
from BaseClasses import Item, MultiWorld, Location, Tutorial
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \
|
||||
basic_unit
|
||||
from .Locations import get_locations
|
||||
from .Regions import create_regions
|
||||
from .Options import sc2wol_options, get_option_value
|
||||
from .LogicMixin import SC2WoLLogic
|
||||
from ..AutoWorld import World
|
||||
|
||||
|
||||
class Starcraft2WoLWebWorld(WebWorld):
|
||||
setup = Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["TheCondor"]
|
||||
)
|
||||
|
||||
tutorials = [setup]
|
||||
|
||||
class SC2WoLWorld(World):
|
||||
"""
|
||||
StarCraft II: Wings of Liberty is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
|
||||
Command Raynor's Raiders in collecting pieces of the Keystone in order to stop the zerg threat posed by the Queen of Blades.
|
||||
"""
|
||||
|
||||
game = "Starcraft 2 Wings of Liberty"
|
||||
web = Starcraft2WoLWebWorld()
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||
options = sc2wol_options
|
||||
|
||||
item_name_groups = item_name_groups
|
||||
locked_locations: typing.List[str]
|
||||
location_cache: typing.List[Location]
|
||||
|
||||
def _get_sc2wol_data(self):
|
||||
return {}
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super(SC2WoLWorld, self).__init__(world, player)
|
||||
self.location_cache = []
|
||||
self.locked_locations = []
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = get_full_item_list()[name]
|
||||
return [self.create_item(name)] * data.quantity
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = get_full_item_list()[name]
|
||||
return StarcraftWoLItem(name, data.progression, data.code, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world, self.player, get_locations(self.world, self.player),
|
||||
self.location_cache)
|
||||
|
||||
def generate_basic(self):
|
||||
excluded_items = get_excluded_items(self, self.world, self.player)
|
||||
|
||||
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
|
||||
|
||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||
|
||||
fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool)
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
def set_rules(self):
|
||||
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
|
||||
|
||||
self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.world.random.choice(filler_items)
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = self._get_sc2wol_data()
|
||||
for option_name in sc2wol_options:
|
||||
option = getattr(self.world, option_name)[self.player]
|
||||
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
|
||||
slot_data[option_name] = int(option.value)
|
||||
return slot_data
|
||||
|
||||
|
||||
def setup_events(world: MultiWorld, player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]):
|
||||
for location in location_cache:
|
||||
if location.address == None:
|
||||
item = Item(location.name, True, None, player)
|
||||
|
||||
locked_locations.append(location.name)
|
||||
|
||||
location.place_locked_item(item)
|
||||
|
||||
|
||||
def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set[str]:
|
||||
excluded_items: Set[str] = set()
|
||||
|
||||
if get_option_value(world, player, "upgrade_bonus") == 1:
|
||||
excluded_items.add("Ultra-Capacitors")
|
||||
else:
|
||||
excluded_items.add("Vanadium Plating")
|
||||
|
||||
if get_option_value(world, player, "bunker_upgrade") == 1:
|
||||
excluded_items.add("Shrike Turret")
|
||||
else:
|
||||
excluded_items.add("Fortified Bunker")
|
||||
|
||||
for item in world.precollected_items[player]:
|
||||
excluded_items.add(item.name)
|
||||
|
||||
return excluded_items
|
||||
|
||||
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||
non_local_items = world.non_local_items[player].value
|
||||
|
||||
local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items)
|
||||
if not local_basic_unit:
|
||||
raise Exception("At least one basic unit must be local")
|
||||
|
||||
assign_starter_item(world, player, excluded_items, locked_locations, 'Liberation Day: First Statue',
|
||||
local_basic_unit)
|
||||
|
||||
|
||||
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
|
||||
location: str, item_list: Tuple[str, ...]):
|
||||
|
||||
item_name = world.random.choice(item_list)
|
||||
|
||||
excluded_items.add(item_name)
|
||||
|
||||
item = create_item_with_correct_settings(world, player, item_name)
|
||||
|
||||
world.get_location(location, player).place_locked_item(item)
|
||||
|
||||
locked_locations.append(location)
|
||||
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||
pool: List[Item] = []
|
||||
|
||||
for name, data in item_table.items():
|
||||
if name not in excluded_items:
|
||||
for _ in range(data.quantity):
|
||||
item = create_item_with_correct_settings(world, player, name)
|
||||
pool.append(item)
|
||||
|
||||
return pool
|
||||
|
||||
def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str],
|
||||
location_cache: List[Location], pool: List[Item]):
|
||||
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
|
||||
item = create_item_with_correct_settings(world, player, self.get_filler_item_name())
|
||||
pool.append(item)
|
||||
|
||||
|
||||
def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item:
|
||||
data = item_table[name]
|
||||
|
||||
item = Item(name, data.progression, data.code, player)
|
||||
item.never_exclude = data.never_exclude
|
||||
|
||||
if not item.advancement:
|
||||
return item
|
||||
|
||||
return item
|
|
@ -0,0 +1,33 @@
|
|||
# Starcraft 2 Wings of Liberty
|
||||
|
||||
## 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?
|
||||
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
|
||||
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
|
||||
would in the vanilla game. All rings and spells are also randomized into those item locations, therefore you can no
|
||||
longer craft them at the alchemist
|
||||
|
||||
## What is the goal of Starcraft 2 when randomized?
|
||||
|
||||
The goal remains unchanged. Beat the final mission All In.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
Unit unlocks, upgrade unlocks, armory upgrades, laboratory researches, and mercenary unlocks can be shuffled, and all
|
||||
bonus objectives, side missions, mission completions are now locations that can contain these items.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
|
||||
certain items to your own world.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item, they will receive a message through their text client and in game if currently playing
|
||||
a mission. They will immediately be able to use that unlock/upgrade.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Starcraft 2 Wings of Liberty Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Starcraft 2](https://starcraft2.com/en-us/)
|
||||
- [Starcraft 2 AP Client](https://github.com/ArchipelagoMW/Archipelago)
|
||||
- [Starcraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData)
|
||||
|
||||
## General Concept
|
||||
|
||||
Starcraft 2 AP Client launches a custom version of Starcraft 2 running modified Wings of Liberty campaign maps
|
||||
to allow for randomization of the items
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Download latest release on [Starcraft 2 Archipelago Data Releases](https://github.com/TheCondor07/Starcraft2ArchipelagoData/releases) you
|
||||
can find the .zip files on the releases page. Download the zip then extract the zip to the
|
||||
folder where your Starcraft 2 game is installed. The just run ArchipelagoStarcraftClient.exe to start the client to
|
||||
connect to a Multiworld Game.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run ArchipelagoStarcraftClient.exe
|
||||
2. Type in /connect [sever ip]
|
||||
3. Insert slot name and password as prompted
|
||||
4. Once connected, use /unfinished to find what missions you can play and '/play [mission id]' to launch a mission. For
|
||||
new games under default settings the first mission available will always be Liberation Day[1] playable using the command
|
||||
'/play 1'
|
||||
|
||||
## Where do I get a config file?
|
||||
|
||||
The [Player Settings](https://archipelago.gg/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on the website allows you to
|
||||
configure your personal settings and export them into a config file
|
|
@ -0,0 +1,3 @@
|
|||
nest-asyncio >= 1.5.5
|
||||
six >= 1.16.0
|
||||
apsc2 >= 5.5
|
Loading…
Reference in New Issue