mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-17 21:13:46 -07:00
Compare commits
2 Commits
factorio_a
...
core_check
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebbfc70c56 | ||
|
|
972ef7d829 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,14 +9,12 @@
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.apemerald
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.sfc
|
||||
*.z64
|
||||
*.n64
|
||||
*.nes
|
||||
*.smc
|
||||
*.sms
|
||||
*.gb
|
||||
*.gbc
|
||||
|
||||
@@ -113,11 +113,6 @@ class MultiWorld():
|
||||
for region in regions:
|
||||
self.region_cache[region.player][region.name] = region
|
||||
|
||||
def add_group(self, new_id: int):
|
||||
self.region_cache[new_id] = {}
|
||||
self.entrance_cache[new_id] = {}
|
||||
self.location_cache[new_id] = {}
|
||||
|
||||
def __iter__(self) -> Iterator[Region]:
|
||||
for regions in self.region_cache.values():
|
||||
yield from regions.values()
|
||||
@@ -225,7 +220,6 @@ class MultiWorld():
|
||||
return group_id, group
|
||||
new_id: int = self.players + len(self.groups) + 1
|
||||
|
||||
self.regions.add_group(new_id)
|
||||
self.game[new_id] = game
|
||||
self.player_types[new_id] = NetUtils.SlotType.group
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||
@@ -623,7 +617,7 @@ class CollectionState():
|
||||
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []
|
||||
|
||||
def __init__(self, parent: MultiWorld):
|
||||
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
|
||||
self.prog_items = {player: Counter() for player in parent.player_ids}
|
||||
self.multiworld = parent
|
||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
||||
@@ -714,43 +708,37 @@ class CollectionState():
|
||||
assert isinstance(event.item, Item), "tried to collect Event with no Item"
|
||||
self.collect(event.item, True, event)
|
||||
|
||||
# item name related
|
||||
def has(self, item: str, player: int, count: int = 1) -> bool:
|
||||
return self.prog_items[player][item] >= count
|
||||
|
||||
def has_all(self, items: Iterable[str], player: int) -> bool:
|
||||
def has_all(self, items: Set[str], player: int) -> bool:
|
||||
"""Returns True if each item name of items is in state at least once."""
|
||||
return all(self.prog_items[player][item] for item in items)
|
||||
|
||||
def has_any(self, items: Iterable[str], player: int) -> bool:
|
||||
def has_any(self, items: Set[str], player: int) -> bool:
|
||||
"""Returns True if at least one item name of items is in state at least once."""
|
||||
return any(self.prog_items[player][item] for item in items)
|
||||
|
||||
def count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[player][item]
|
||||
|
||||
def item_count(self, item: str, player: int) -> int:
|
||||
Utils.deprecate("Use count instead.")
|
||||
return self.count(item, player)
|
||||
|
||||
# item name group related
|
||||
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
found += player_prog_items[item_name]
|
||||
found += self.prog_items[player][item_name]
|
||||
if found >= count:
|
||||
return True
|
||||
return False
|
||||
|
||||
def count_group(self, item_name_group: str, player: int) -> int:
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
found += player_prog_items[item_name]
|
||||
found += self.prog_items[player][item_name]
|
||||
return found
|
||||
|
||||
# Item related
|
||||
def item_count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[player][item]
|
||||
|
||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
|
||||
@@ -737,8 +737,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
elif 'InvalidGame' in errors:
|
||||
ctx.event_invalid_game()
|
||||
elif 'IncompatibleVersion' in errors:
|
||||
raise Exception('Server reported your client version as incompatible. '
|
||||
'This probably means you have to update.')
|
||||
raise Exception('Server reported your client version as incompatible')
|
||||
elif 'InvalidItemsHandling' in errors:
|
||||
raise Exception('The item handling flags requested by the client are not supported')
|
||||
# last to check, recoverable problem
|
||||
@@ -759,7 +758,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
|
||||
ctx.hint_points = args.get("hint_points", 0)
|
||||
ctx.consume_players_package(args["players"])
|
||||
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
|
||||
msgs = []
|
||||
if ctx.locations_checked:
|
||||
msgs.append({"cmd": "LocationChecks",
|
||||
@@ -838,14 +836,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
|
||||
elif cmd == "Retrieved":
|
||||
ctx.stored_data.update(args["keys"])
|
||||
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
ctx.ui.update_hints()
|
||||
|
||||
elif cmd == "SetReply":
|
||||
ctx.stored_data[args["key"]] = args["value"]
|
||||
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]:
|
||||
ctx.ui.update_hints()
|
||||
elif args["key"].startswith("EnergyLink"):
|
||||
if args["key"].startswith("EnergyLink"):
|
||||
ctx.current_energy_link_value = args["value"]
|
||||
if ctx.ui:
|
||||
ctx.ui.set_new_energy_link_value()
|
||||
|
||||
7
Fill.py
7
Fill.py
@@ -112,7 +112,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
location.item = None
|
||||
placed_item.location = None
|
||||
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool)
|
||||
swap_state = sweep_from_pool(base_state, [placed_item] if unsafe else [])
|
||||
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
|
||||
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
|
||||
# to clean that up later, so there is a chance generation fails.
|
||||
@@ -471,7 +471,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
restitempool = usefulitempool + filleritempool
|
||||
|
||||
remaining_fill(world, defaultlocations, restitempool)
|
||||
|
||||
@@ -792,9 +792,6 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
block['force'] = 'silent'
|
||||
if 'from_pool' not in block:
|
||||
block['from_pool'] = True
|
||||
elif not isinstance(block['from_pool'], bool):
|
||||
from_pool_type = type(block['from_pool'])
|
||||
raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.')
|
||||
if 'world' not in block:
|
||||
target_world = False
|
||||
else:
|
||||
|
||||
13
Generate.py
13
Generate.py
@@ -20,7 +20,7 @@ import Options
|
||||
from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
from Main import main as ERmain
|
||||
from settings import get_settings
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, user_path
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from worlds.alttp.Text import TextTable
|
||||
@@ -53,9 +53,6 @@ def mystery_argparse():
|
||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||
parser.add_argument("--skip_prog_balancing", action="store_true",
|
||||
help="Skip progression balancing step during generation.")
|
||||
parser.add_argument("--skip_output", action="store_true",
|
||||
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
||||
"Intended for debugging and testing purposes.")
|
||||
args = parser.parse_args()
|
||||
if not os.path.isabs(args.weights_file_path):
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
@@ -130,13 +127,6 @@ def main(args=None, callback=ERmain):
|
||||
player_id += 1
|
||||
|
||||
args.multi = max(player_id - 1, args.multi)
|
||||
|
||||
if args.multi == 0:
|
||||
raise ValueError(
|
||||
"No individual player files found and number of players is 0. "
|
||||
"Provide individual player files or specify the number of players via host.yaml or --multi."
|
||||
)
|
||||
|
||||
logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, "
|
||||
f"{seed_name} Seed {seed} with plando: {args.plando}")
|
||||
|
||||
@@ -153,7 +143,6 @@ def main(args=None, callback=ERmain):
|
||||
erargs.outputname = seed_name
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||
erargs.skip_output = args.skip_output
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
|
||||
892
KH2Client.py
892
KH2Client.py
@@ -1,8 +1,894 @@
|
||||
import os
|
||||
import asyncio
|
||||
import ModuleUpdate
|
||||
import json
|
||||
import Utils
|
||||
from worlds.kh2.Client import launch
|
||||
from pymem import pymem
|
||||
from worlds.kh2.Items import exclusionItem_table, CheckDupingItems
|
||||
from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table
|
||||
|
||||
from worlds.kh2.WorldLocations import *
|
||||
|
||||
from worlds import network_data_package
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("KH2Client", exception_logger="Client")
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
|
||||
CommonContext, server_loop
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||
|
||||
|
||||
# class KH2CommandProcessor(ClientCommandProcessor):
|
||||
|
||||
|
||||
class KH2Context(CommonContext):
|
||||
# command_processor: int = KH2CommandProcessor
|
||||
game = "Kingdom Hearts 2"
|
||||
items_handling = 0b101 # Indicates you get items sent from other worlds.
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(KH2Context, self).__init__(server_address, password)
|
||||
self.kh2LocalItems = None
|
||||
self.ability = None
|
||||
self.growthlevel = None
|
||||
self.KH2_sync_task = None
|
||||
self.syncing = False
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in
|
||||
item_dictionary_table.items() if data.code}
|
||||
self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in
|
||||
all_locations.items() if data.code}
|
||||
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
|
||||
|
||||
self.location_table = {}
|
||||
self.collectible_table = {}
|
||||
self.collectible_override_flags_address = 0
|
||||
self.collectible_offsets = {}
|
||||
self.sending = []
|
||||
# list used to keep track of locations+items player has. Used for disoneccting
|
||||
self.kh2seedsave = None
|
||||
self.slotDataProgressionNames = {}
|
||||
self.kh2seedname = None
|
||||
self.kh2slotdata = None
|
||||
self.itemamount = {}
|
||||
# sora equipped, valor equipped, master equipped, final equipped
|
||||
self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4)
|
||||
if "localappdata" in os.environ:
|
||||
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
|
||||
self.amountOfPieces = 0
|
||||
# hooked object
|
||||
self.kh2 = None
|
||||
self.ItemIsSafe = False
|
||||
self.game_connected = False
|
||||
self.finalxemnas = False
|
||||
self.worldid = {
|
||||
# 1: {}, # world of darkness (story cutscenes)
|
||||
2: TT_Checks,
|
||||
# 3: {}, # destiny island doesn't have checks to ima put tt checks here
|
||||
4: HB_Checks,
|
||||
5: BC_Checks,
|
||||
6: Oc_Checks,
|
||||
7: AG_Checks,
|
||||
8: LoD_Checks,
|
||||
9: HundredAcreChecks,
|
||||
10: PL_Checks,
|
||||
11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc
|
||||
12: DC_Checks,
|
||||
13: TR_Checks,
|
||||
14: HT_Checks,
|
||||
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
|
||||
16: PR_Checks,
|
||||
17: SP_Checks,
|
||||
18: TWTNW_Checks,
|
||||
# 255: {}, # starting screen
|
||||
}
|
||||
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
|
||||
self.sveroom = 0x2A09C00 + 0x41
|
||||
# 0 not in battle 1 in yellow battle 2 red battle #short
|
||||
self.inBattle = 0x2A0EAC4 + 0x40
|
||||
self.onDeath = 0xAB9078
|
||||
# PC Address anchors
|
||||
self.Now = 0x0714DB8
|
||||
self.Save = 0x09A70B0
|
||||
self.Sys3 = 0x2A59DF0
|
||||
self.Bt10 = 0x2A74880
|
||||
self.BtlEnd = 0x2A0D3E0
|
||||
self.Slot1 = 0x2A20C98
|
||||
|
||||
self.chest_set = set(exclusion_table["Chests"])
|
||||
|
||||
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
|
||||
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
|
||||
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
|
||||
|
||||
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
|
||||
|
||||
self.equipment_categories = CheckDupingItems["Equipment"]
|
||||
self.armor_set = set(self.equipment_categories["Armor"])
|
||||
self.accessories_set = set(self.equipment_categories["Accessories"])
|
||||
self.all_equipment = self.armor_set.union(self.accessories_set)
|
||||
|
||||
self.Equipment_Anchor_Dict = {
|
||||
"Armor": [0x2504, 0x2506, 0x2508, 0x250A],
|
||||
"Accessories": [0x2514, 0x2516, 0x2518, 0x251A]}
|
||||
|
||||
self.AbilityQuantityDict = {}
|
||||
self.ability_categories = CheckDupingItems["Abilities"]
|
||||
|
||||
self.sora_ability_set = set(self.ability_categories["Sora"])
|
||||
self.donald_ability_set = set(self.ability_categories["Donald"])
|
||||
self.goofy_ability_set = set(self.ability_categories["Goofy"])
|
||||
|
||||
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
|
||||
|
||||
self.boost_set = set(CheckDupingItems["Boosts"])
|
||||
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
|
||||
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
|
||||
# Growth:[level 1,level 4,slot]
|
||||
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25DA],
|
||||
"Quick Run": [0x62, 0x65, 0x25DC],
|
||||
"Dodge Roll": [0x234, 0x237, 0x25DE],
|
||||
"Aerial Dodge": [0x066, 0x069, 0x25E0],
|
||||
"Glide": [0x6A, 0x6D, 0x25E2]}
|
||||
self.boost_to_anchor_dict = {
|
||||
"Power Boost": 0x24F9,
|
||||
"Magic Boost": 0x24FA,
|
||||
"Defense Boost": 0x24FB,
|
||||
"AP Boost": 0x24F8}
|
||||
|
||||
self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]]
|
||||
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
|
||||
|
||||
self.bitmask_item_code = [
|
||||
0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007
|
||||
, 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C
|
||||
, 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023
|
||||
, 0x13002A, 0x13002B, 0x13002C, 0x13002D]
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(KH2Context, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
async def connection_closed(self):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname is not None and self.auth is not None:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||
await super(KH2Context, self).connection_closed()
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||
await super(KH2Context, self).disconnect()
|
||||
|
||||
@property
|
||||
def endpoints(self):
|
||||
if self.server:
|
||||
return [self.server]
|
||||
else:
|
||||
return []
|
||||
|
||||
async def shutdown(self):
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||
await super(KH2Context, self).shutdown()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"RoomInfo"}:
|
||||
self.kh2seedname = args['seed_name']
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||
self.kh2seedsave = {"itemIndex": -1,
|
||||
# back of soras invo is 0x25E2. Growth should be moved there
|
||||
# Character: [back of invo, front of invo]
|
||||
"SoraInvo": [0x25D8, 0x2546],
|
||||
"DonaldInvo": [0x26F4, 0x2658],
|
||||
"GoofyInvo": [0x280A, 0x276C],
|
||||
"AmountInvo": {
|
||||
"ServerItems": {
|
||||
"Ability": {},
|
||||
"Amount": {},
|
||||
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
|
||||
"Aerial Dodge": 0,
|
||||
"Glide": 0},
|
||||
"Bitmask": [],
|
||||
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||
"Equipment": [],
|
||||
"Magic": {},
|
||||
"StatIncrease": {},
|
||||
"Boost": {},
|
||||
},
|
||||
"LocalItems": {
|
||||
"Ability": {},
|
||||
"Amount": {},
|
||||
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
|
||||
"Aerial Dodge": 0, "Glide": 0},
|
||||
"Bitmask": [],
|
||||
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||
"Equipment": [],
|
||||
"Magic": {},
|
||||
"StatIncrease": {},
|
||||
"Boost": {},
|
||||
}},
|
||||
# 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked
|
||||
"LocationsChecked": [],
|
||||
"Levels": {
|
||||
"SoraLevel": 0,
|
||||
"ValorLevel": 0,
|
||||
"WisdomLevel": 0,
|
||||
"LimitLevel": 0,
|
||||
"MasterLevel": 0,
|
||||
"FinalLevel": 0,
|
||||
},
|
||||
"SoldEquipment": [],
|
||||
"SoldBoosts": {"Power Boost": 0,
|
||||
"Magic Boost": 0,
|
||||
"Defense Boost": 0,
|
||||
"AP Boost": 0}
|
||||
}
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'wt') as f:
|
||||
pass
|
||||
self.locations_checked = set()
|
||||
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
|
||||
self.kh2seedsave = json.load(f)
|
||||
self.locations_checked = set(self.kh2seedsave["LocationsChecked"])
|
||||
self.serverconneced = True
|
||||
|
||||
if cmd in {"Connected"}:
|
||||
self.kh2slotdata = args['slot_data']
|
||||
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
||||
try:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
logger.info("You are now auto-tracking")
|
||||
self.kh2connected = True
|
||||
except Exception as e:
|
||||
logger.info("Line 247")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
if cmd in {"ReceivedItems"}:
|
||||
start_index = args["index"]
|
||||
if start_index == 0:
|
||||
# resetting everything that were sent from the server
|
||||
self.kh2seedsave["SoraInvo"][0] = 0x25D8
|
||||
self.kh2seedsave["DonaldInvo"][0] = 0x26F4
|
||||
self.kh2seedsave["GoofyInvo"][0] = 0x280A
|
||||
self.kh2seedsave["itemIndex"] = - 1
|
||||
self.kh2seedsave["AmountInvo"]["ServerItems"] = {
|
||||
"Ability": {},
|
||||
"Amount": {},
|
||||
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
|
||||
"Aerial Dodge": 0,
|
||||
"Glide": 0},
|
||||
"Bitmask": [],
|
||||
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||
"Equipment": [],
|
||||
"Magic": {},
|
||||
"StatIncrease": {},
|
||||
"Boost": {},
|
||||
}
|
||||
if start_index > self.kh2seedsave["itemIndex"]:
|
||||
self.kh2seedsave["itemIndex"] = start_index
|
||||
for item in args['items']:
|
||||
asyncio.create_task(self.give_item(item.item))
|
||||
|
||||
if cmd in {"RoomUpdate"}:
|
||||
if "checked_locations" in args:
|
||||
new_locations = set(args["checked_locations"])
|
||||
# TODO: make this take locations from other players on the same slot so proper coop happens
|
||||
# items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if
|
||||
# location_id in self.kh2LocalItems.keys()]
|
||||
self.checked_locations |= new_locations
|
||||
|
||||
async def checkWorldLocations(self):
|
||||
try:
|
||||
currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")
|
||||
if currentworldint in self.worldid:
|
||||
curworldid = self.worldid[currentworldint]
|
||||
for location, data in curworldid.items():
|
||||
locationId = kh2_loc_name_to_id[location]
|
||||
if locationId not in self.locations_checked \
|
||||
and (int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") & 0x1 << data.bitIndex) > 0:
|
||||
self.sending = self.sending + [(int(locationId))]
|
||||
except Exception as e:
|
||||
logger.info("Line 285")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def checkLevels(self):
|
||||
try:
|
||||
for location, data in SoraLevels.items():
|
||||
currentLevel = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
|
||||
locationId = kh2_loc_name_to_id[location]
|
||||
if locationId not in self.locations_checked \
|
||||
and currentLevel >= data.bitIndex:
|
||||
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
|
||||
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
|
||||
self.sending = self.sending + [(int(locationId))]
|
||||
formDict = {
|
||||
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
|
||||
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
|
||||
for i in range(5):
|
||||
for location, data in formDict[i][1].items():
|
||||
formlevel = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
|
||||
locationId = kh2_loc_name_to_id[location]
|
||||
if locationId not in self.locations_checked \
|
||||
and formlevel >= data.bitIndex:
|
||||
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
|
||||
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
|
||||
self.sending = self.sending + [(int(locationId))]
|
||||
except Exception as e:
|
||||
logger.info("Line 312")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def checkSlots(self):
|
||||
try:
|
||||
for location, data in weaponSlots.items():
|
||||
locationId = kh2_loc_name_to_id[location]
|
||||
if locationId not in self.locations_checked:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") > 0:
|
||||
self.sending = self.sending + [(int(locationId))]
|
||||
|
||||
for location, data in formSlots.items():
|
||||
locationId = kh2_loc_name_to_id[location]
|
||||
if locationId not in self.locations_checked:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") & 0x1 << data.bitIndex > 0:
|
||||
# self.locations_checked
|
||||
self.sending = self.sending + [(int(locationId))]
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
logger.info("Line 333")
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def verifyChests(self):
|
||||
try:
|
||||
for location in self.locations_checked:
|
||||
locationName = self.lookup_id_to_Location[location]
|
||||
if locationName in self.chest_set:
|
||||
if locationName in self.location_name_to_worlddata.keys():
|
||||
locationData = self.location_name_to_worlddata[locationName]
|
||||
if int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||
"big") & 0x1 << locationData.bitIndex == 0:
|
||||
roomData = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
|
||||
1), "big")
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
|
||||
(roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1)
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
logger.info("Line 350")
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def verifyLevel(self):
|
||||
for leveltype, anchor in {"SoraLevel": 0x24FF,
|
||||
"ValorLevel": 0x32F6,
|
||||
"WisdomLevel": 0x332E,
|
||||
"LimitLevel": 0x3366,
|
||||
"MasterLevel": 0x339E,
|
||||
"FinalLevel": 0x33D6}.items():
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \
|
||||
self.kh2seedsave["Levels"][leveltype]:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
|
||||
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
|
||||
|
||||
async def give_item(self, item, ItemType="ServerItems"):
|
||||
try:
|
||||
itemname = self.lookup_id_to_item[item]
|
||||
itemcode = self.item_name_to_data[itemname]
|
||||
if itemcode.ability:
|
||||
abilityInvoType = 0
|
||||
TwilightZone = 2
|
||||
if ItemType == "LocalItems":
|
||||
abilityInvoType = 1
|
||||
TwilightZone = -2
|
||||
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1
|
||||
return
|
||||
|
||||
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = []
|
||||
# appending the slot that the ability should be in
|
||||
|
||||
if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \
|
||||
self.AbilityQuantityDict[itemname]:
|
||||
if itemname in self.sora_ability_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||
self.kh2seedsave["SoraInvo"][abilityInvoType])
|
||||
self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone
|
||||
elif itemname in self.donald_ability_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||
self.kh2seedsave["DonaldInvo"][abilityInvoType])
|
||||
self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||
self.kh2seedsave["GoofyInvo"][abilityInvoType])
|
||||
self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone
|
||||
|
||||
elif itemcode.code in self.bitmask_item_code:
|
||||
|
||||
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname)
|
||||
|
||||
elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
|
||||
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1
|
||||
elif itemname in self.all_equipment:
|
||||
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname)
|
||||
|
||||
elif itemname in self.all_weapons:
|
||||
if itemname in self.keyblade_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname)
|
||||
elif itemname in self.staff_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname)
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname)
|
||||
|
||||
elif itemname in self.boost_set:
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1
|
||||
|
||||
elif itemname in self.stat_increase_set:
|
||||
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1
|
||||
|
||||
else:
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
logger.info("Line 398")
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
from kvui import GameManager
|
||||
|
||||
class KH2Manager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago KH2 Client"
|
||||
|
||||
self.ui = KH2Manager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def IsInShop(self, sellable, master_boost):
|
||||
# journal = 0x741230 shop = 0x741320
|
||||
# if journal=-1 and shop = 5 then in shop
|
||||
# if journam !=-1 and shop = 10 then journal
|
||||
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
|
||||
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
|
||||
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||
# print("your in the shop")
|
||||
sellable_dict = {}
|
||||
for itemName in sellable:
|
||||
itemdata = self.item_name_to_data[itemName]
|
||||
amount = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
|
||||
sellable_dict[itemName] = amount
|
||||
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
|
||||
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
|
||||
await asyncio.sleep(0.5)
|
||||
for item, amount in sellable_dict.items():
|
||||
itemdata = self.item_name_to_data[item]
|
||||
afterShop = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
|
||||
if afterShop < amount:
|
||||
if item in master_boost:
|
||||
self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop)
|
||||
else:
|
||||
self.kh2seedsave["SoldEquipment"].append(item)
|
||||
|
||||
async def verifyItems(self):
|
||||
try:
|
||||
local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys())
|
||||
server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys())
|
||||
master_amount = local_amount | server_amount
|
||||
|
||||
local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys())
|
||||
server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys())
|
||||
master_ability = local_ability | server_ability
|
||||
|
||||
local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"])
|
||||
server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"])
|
||||
master_bitmask = local_bitmask | server_bitmask
|
||||
|
||||
local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"])
|
||||
local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"])
|
||||
local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"])
|
||||
|
||||
server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"])
|
||||
server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"])
|
||||
server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"])
|
||||
|
||||
master_keyblade = local_keyblade | server_keyblade
|
||||
master_staff = local_staff | server_staff
|
||||
master_shield = local_shield | server_shield
|
||||
|
||||
local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"])
|
||||
server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"])
|
||||
master_equipment = local_equipment | server_equipment
|
||||
|
||||
local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys())
|
||||
server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys())
|
||||
master_magic = local_magic | server_magic
|
||||
|
||||
local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys())
|
||||
server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys())
|
||||
master_stat = local_stat | server_stat
|
||||
|
||||
local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys())
|
||||
server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys())
|
||||
master_boost = local_boost | server_boost
|
||||
|
||||
master_sell = master_equipment | master_staff | master_shield | master_boost
|
||||
await asyncio.create_task(self.IsInShop(master_sell, master_boost))
|
||||
for itemName in master_amount:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_amount:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName]
|
||||
if itemName in server_amount:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName]
|
||||
|
||||
if itemName == "Torn Page":
|
||||
# Torn Pages are handled differently because they can be consumed.
|
||||
# Will check the progression in 100 acre and - the amount of visits
|
||||
# amountofitems-amount of visits done
|
||||
for location, data in tornPageLocks.items():
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") & 0x1 << data.bitIndex > 0:
|
||||
amountOfItems -= 1
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != amountOfItems and amountOfItems >= 0:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
amountOfItems.to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_keyblade:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
# if the inventory slot for that keyblade is less than the amount they should have
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1),
|
||||
"big") != 13:
|
||||
# Checking form anchors for the keyblade
|
||||
if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \
|
||||
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \
|
||||
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \
|
||||
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(0).to_bytes(1, 'big'), 1)
|
||||
else:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
for itemName in master_staff:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1 \
|
||||
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \
|
||||
and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_shield:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1 \
|
||||
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \
|
||||
and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_ability:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
ability_slot = []
|
||||
if itemName in local_ability:
|
||||
ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName]
|
||||
if itemName in server_ability:
|
||||
ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName]
|
||||
for slot in ability_slot:
|
||||
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||
ability = current & 0x0FFF
|
||||
if ability | 0x8000 != (0x8000 + itemData.memaddr):
|
||||
if current - 0x8000 > 0:
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, (0x8000 + itemData.memaddr))
|
||||
else:
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
|
||||
# removes the duped ability if client gave faster than the game.
|
||||
for charInvo in {"SoraInvo", "DonaldInvo", "GoofyInvo"}:
|
||||
if self.kh2.read_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1]) != 0 and \
|
||||
self.kh2seedsave[charInvo][1] + 2 < self.kh2seedsave[charInvo][0]:
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1], 0)
|
||||
# remove the dummy level 1 growths if they are in these invo slots.
|
||||
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
|
||||
current = self.kh2.read_short(self.kh2.base_address + self.Save + inventorySlot)
|
||||
ability = current & 0x0FFF
|
||||
if 0x05E <= ability <= 0x06D:
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + inventorySlot, 0)
|
||||
|
||||
for itemName in self.master_growth:
|
||||
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
|
||||
+ self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName]
|
||||
if growthLevel > 0:
|
||||
slot = self.growth_values_dict[itemName][2]
|
||||
min_growth = self.growth_values_dict[itemName][0]
|
||||
max_growth = self.growth_values_dict[itemName][1]
|
||||
if growthLevel > 4:
|
||||
growthLevel = 4
|
||||
current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||
ability = current_growth_level & 0x0FFF
|
||||
# if the player should be getting a growth ability
|
||||
if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
|
||||
# if it should be level one of that growth
|
||||
if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth)
|
||||
# if it is already in the inventory
|
||||
elif ability | 0x8000 < (0x8000 + max_growth):
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1)
|
||||
|
||||
for itemName in master_bitmask:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
itemMemory = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
|
||||
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") & 0x1 << itemData.bitmask) == 0:
|
||||
# when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game.
|
||||
if itemName in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + 0x3410,
|
||||
(0).to_bytes(1, 'big'), 1)
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_equipment:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
isThere = False
|
||||
if itemName in self.accessories_set:
|
||||
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
|
||||
else:
|
||||
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
|
||||
# Checking form anchors for the equipment
|
||||
for slot in Equipment_Anchor_List:
|
||||
if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id:
|
||||
isThere = True
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 0:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(0).to_bytes(1, 'big'), 1)
|
||||
break
|
||||
if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_magic:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_magic:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName]
|
||||
if itemName in server_magic:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName]
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != amountOfItems \
|
||||
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
amountOfItems.to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_stat:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_stat:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName]
|
||||
if itemName in server_stat:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
|
||||
|
||||
# 0x130293 is Crit_1's location id for touching the computer
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != amountOfItems \
|
||||
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
|
||||
"big") >= 5 and int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x23DF, 1),
|
||||
"big") > 0:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
amountOfItems.to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_boost:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_boost:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName]
|
||||
if itemName in server_boost:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName]
|
||||
amountOfBoostsInInvo = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big")
|
||||
amountOfUsedBoosts = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1),
|
||||
"big")
|
||||
# Ap Boots start at +50 for some reason
|
||||
if itemName == "AP Boost":
|
||||
amountOfUsedBoosts -= 50
|
||||
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
|
||||
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][
|
||||
itemName] and amountOfBoostsInInvo < 255:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
|
||||
|
||||
except Exception as e:
|
||||
logger.info("Line 573")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
|
||||
def finishedGame(ctx: KH2Context, message):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if 0x1301ED in message[0]["locations"]:
|
||||
ctx.finalxemnas = True
|
||||
# three proofs
|
||||
if ctx.kh2slotdata['Goal'] == 0:
|
||||
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \
|
||||
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \
|
||||
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0:
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if ctx.finalxemnas:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif ctx.kh2slotdata['Goal'] == 1:
|
||||
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \
|
||||
ctx.kh2slotdata['LuckyEmblemsRequired']:
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if ctx.finalxemnas:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif ctx.kh2slotdata['Goal'] == 2:
|
||||
for boss in ctx.kh2slotdata["hitlist"]:
|
||||
if boss in message[0]["locations"]:
|
||||
ctx.amountOfPieces += 1
|
||||
if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]:
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if ctx.finalxemnas:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
async def kh2_watcher(ctx: KH2Context):
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
if ctx.kh2connected and ctx.serverconneced:
|
||||
ctx.sending = []
|
||||
await asyncio.create_task(ctx.checkWorldLocations())
|
||||
await asyncio.create_task(ctx.checkLevels())
|
||||
await asyncio.create_task(ctx.checkSlots())
|
||||
await asyncio.create_task(ctx.verifyChests())
|
||||
await asyncio.create_task(ctx.verifyItems())
|
||||
await asyncio.create_task(ctx.verifyLevel())
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
if finishedGame(ctx, message):
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
location_ids = []
|
||||
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
|
||||
for location in location_ids:
|
||||
if location not in ctx.locations_checked:
|
||||
ctx.locations_checked.add(location)
|
||||
ctx.kh2seedsave["LocationsChecked"].append(location)
|
||||
if location in ctx.kh2LocalItems:
|
||||
item = ctx.kh2slotdata["LocalItems"][str(location)]
|
||||
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
|
||||
await ctx.send_msgs(message)
|
||||
elif not ctx.kh2connected and ctx.serverconneced:
|
||||
logger.info("Game is not open. Disconnecting from Server.")
|
||||
await ctx.disconnect()
|
||||
except Exception as e:
|
||||
logger.info("Line 661")
|
||||
if ctx.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
ctx.kh2connected = False
|
||||
logger.info(e)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Utils.init_logging("KH2Client", exception_logger="Client")
|
||||
launch()
|
||||
async def main(args):
|
||||
ctx = KH2Context(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(
|
||||
kh2_watcher(ctx), name="KH2ProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await progression_watcher
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser(description="KH2 Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -58,7 +58,7 @@ class MMBN3CommandProcessor(ClientCommandProcessor):
|
||||
class MMBN3Context(CommonContext):
|
||||
command_processor = MMBN3CommandProcessor
|
||||
game = "MegaMan Battle Network 3"
|
||||
items_handling = 0b101 # full local except starting items
|
||||
items_handling = 0b001 # full local
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
|
||||
19
Main.py
19
Main.py
@@ -13,8 +13,8 @@ import worlds
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||
from Options import StartInventoryPool
|
||||
from Utils import __version__, output_path, version_tuple
|
||||
from settings import get_settings
|
||||
from Utils import __version__, output_path, version_tuple
|
||||
from worlds import AutoWorld
|
||||
from worlds.generic.Rules import exclusion_rules, locality_rules
|
||||
|
||||
@@ -101,9 +101,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
del item_digits, location_digits, item_count, location_count
|
||||
|
||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||
if not args.skip_output:
|
||||
AutoWorld.call_stage(world, "assert_generate")
|
||||
AutoWorld.call_stage(world, "assert_generate")
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
@@ -289,14 +287,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
logger.info("Progression balancing skipped.")
|
||||
|
||||
logger.info(f'Beginning output...')
|
||||
|
||||
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||
world.random.passthrough = False
|
||||
|
||||
if args.skip_output:
|
||||
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
@@ -359,9 +354,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
assert location.item.code is not None, "item code None should be event, " \
|
||||
"location.address should then also be None. Location: " \
|
||||
f" {location}"
|
||||
assert location.address not in locations_data[location.player], (
|
||||
f"Locations with duplicate address. {location} and "
|
||||
f"{locations_data[location.player][location.address]}")
|
||||
locations_data[location.player][location.address] = \
|
||||
location.item.code, location.item.player, location.item.flags
|
||||
if location.name in world.worlds[location.player].options.start_location_hints:
|
||||
@@ -371,6 +363,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
elif any([location.item.name in world.worlds[player].options.start_hints
|
||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||
precollect_hint(location)
|
||||
elif __debug__ and location.item.code is not None:
|
||||
raise Exception(f"Intended to be sendable item {location.item}, "
|
||||
f"was placed on never sendable location {location} of {location.game}.")
|
||||
|
||||
# embedded data package
|
||||
data_package = {
|
||||
|
||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import collections
|
||||
import copy
|
||||
import collections
|
||||
import datetime
|
||||
import functools
|
||||
import hashlib
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import pickle
|
||||
import random
|
||||
@@ -68,25 +67,21 @@ def update_dict(dictionary, entries):
|
||||
|
||||
# functions callable on storable data on the server by clients
|
||||
modify_functions = {
|
||||
# generic:
|
||||
"replace": lambda old, new: new,
|
||||
"default": lambda old, new: old,
|
||||
# numeric:
|
||||
"add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append)
|
||||
"mul": operator.mul,
|
||||
"pow": operator.pow,
|
||||
"mod": operator.mod,
|
||||
"floor": lambda value, _: math.floor(value),
|
||||
"ceil": lambda value, _: math.ceil(value),
|
||||
"max": max,
|
||||
"min": min,
|
||||
"replace": lambda old, new: new,
|
||||
"default": lambda old, new: old,
|
||||
"pow": operator.pow,
|
||||
# bitwise:
|
||||
"xor": operator.xor,
|
||||
"or": operator.or_,
|
||||
"and": operator.and_,
|
||||
"left_shift": operator.lshift,
|
||||
"right_shift": operator.rshift,
|
||||
# lists/dicts:
|
||||
# lists/dicts
|
||||
"remove": remove_from_list,
|
||||
"pop": pop_from_container,
|
||||
"update": update_dict,
|
||||
@@ -417,8 +412,6 @@ class Context:
|
||||
self.player_name_lookup[slot_info.name] = 0, slot_id
|
||||
self.read_data[f"hints_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \
|
||||
list(self.get_rechecked_hints(local_team, local_player))
|
||||
self.read_data[f"client_status_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \
|
||||
self.client_game_state[local_team, local_player]
|
||||
|
||||
self.seed_name = decoded_obj["seed_name"]
|
||||
self.random.seed(self.seed_name)
|
||||
@@ -714,12 +707,6 @@ class Context:
|
||||
"hint_points": get_slot_points(self, team, slot)
|
||||
}])
|
||||
|
||||
def on_client_status_change(self, team: int, slot: int):
|
||||
key: str = f"_read_client_status_{team}_{slot}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
if targets:
|
||||
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.client_game_state[team, slot]}])
|
||||
|
||||
|
||||
def update_aliases(ctx: Context, team: int):
|
||||
cmd = ctx.dumper([{"cmd": "RoomUpdate",
|
||||
@@ -1827,7 +1814,6 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
||||
ctx.on_goal_achieved(client)
|
||||
|
||||
ctx.client_game_state[client.team, client.slot] = new_status
|
||||
ctx.on_client_status_change(client.team, client.slot)
|
||||
ctx.save()
|
||||
|
||||
|
||||
|
||||
34
Options.py
34
Options.py
@@ -696,19 +696,11 @@ class Range(NumericOption):
|
||||
return int(round(random.triangular(lower, end, tri), 0))
|
||||
|
||||
|
||||
class NamedRange(Range):
|
||||
class SpecialRange(Range):
|
||||
special_range_cutoff = 0
|
||||
special_range_names: typing.Dict[str, int] = {}
|
||||
"""Special Range names have to be all lowercase as matching is done with text.lower()"""
|
||||
|
||||
def __init__(self, value: int) -> None:
|
||||
if value < self.range_start and value not in self.special_range_names.values():
|
||||
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__} " +
|
||||
f"and is also not one of the supported named special values: {self.special_range_names}")
|
||||
elif value > self.range_end and value not in self.special_range_names.values():
|
||||
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
|
||||
f"and is also not one of the supported named special values: {self.special_range_names}")
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Range:
|
||||
text = text.lower()
|
||||
@@ -716,19 +708,6 @@ class NamedRange(Range):
|
||||
return cls(cls.special_range_names[text])
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
class SpecialRange(NamedRange):
|
||||
special_range_cutoff = 0
|
||||
|
||||
# TODO: remove class SpecialRange, earliest 3 releases after 0.4.3
|
||||
def __new__(cls, value: int) -> SpecialRange:
|
||||
from Utils import deprecate
|
||||
deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. "
|
||||
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
|
||||
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
|
||||
"placed anywhere (below, inside, or above the regular range).")
|
||||
return super().__new__(cls, value)
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
if text == "random-low":
|
||||
@@ -912,7 +891,7 @@ class Accessibility(Choice):
|
||||
default = 1
|
||||
|
||||
|
||||
class ProgressionBalancing(NamedRange):
|
||||
class ProgressionBalancing(SpecialRange):
|
||||
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
A lower setting means more getting stuck. A higher setting means less getting stuck."""
|
||||
default = 50
|
||||
@@ -1033,11 +1012,6 @@ class DeathLink(Toggle):
|
||||
display_name = "Death Link"
|
||||
|
||||
|
||||
class AllowCollect(DefaultOnToggle):
|
||||
"""Allows checks in your world to be automatically marked as collected when !collect is run."""
|
||||
display_name = "Allow Collect"
|
||||
|
||||
|
||||
class ItemLinks(OptionList):
|
||||
"""Share part of your item pool with other players."""
|
||||
display_name = "Item Links"
|
||||
@@ -1134,7 +1108,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||
os.unlink(full_path)
|
||||
|
||||
def dictify_range(option: Range):
|
||||
def dictify_range(option: typing.Union[Range, SpecialRange]):
|
||||
data = {option.default: 50}
|
||||
for sub_option in ["random", "random-low", "random-high"]:
|
||||
if sub_option != option.default:
|
||||
|
||||
382
PokemonClient.py
Normal file
382
PokemonClient.py
Normal file
@@ -0,0 +1,382 @@
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import bsdiff4
|
||||
import subprocess
|
||||
import zipfile
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
from worlds.pokemon_rb.locations import location_data
|
||||
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
|
||||
|
||||
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
|
||||
location_bytes_bits = {}
|
||||
for location in location_data:
|
||||
if location.ram_address is not None:
|
||||
if type(location.ram_address) == list:
|
||||
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
|
||||
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
|
||||
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
|
||||
else:
|
||||
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
|
||||
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
|
||||
|
||||
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
|
||||
and location.address is not None}
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
|
||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
|
||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
|
||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
|
||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
DISPLAY_MSGS = True
|
||||
|
||||
SCRIPT_VERSION = 3
|
||||
|
||||
|
||||
class GBCommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_gb(self):
|
||||
"""Check Gameboy Connection State"""
|
||||
if isinstance(self.ctx, GBContext):
|
||||
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
|
||||
|
||||
|
||||
class GBContext(CommonContext):
|
||||
command_processor = GBCommandProcessor
|
||||
game = 'Pokemon Red and Blue'
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.gb_streams: (StreamReader, StreamWriter) = None
|
||||
self.gb_sync_task = None
|
||||
self.messages = {}
|
||||
self.locations_array = None
|
||||
self.gb_status = CONNECTION_INITIAL_STATUS
|
||||
self.awaiting_rom = False
|
||||
self.display_msgs = True
|
||||
self.deathlink_pending = False
|
||||
self.set_deathlink = False
|
||||
self.client_compatibility_mode = 0
|
||||
self.items_handling = 0b001
|
||||
self.sent_release = False
|
||||
self.sent_collect = False
|
||||
self.auto_hints = set()
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(GBContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
self.awaiting_rom = True
|
||||
logger.info('Awaiting connection to EmuHawk to get Player information')
|
||||
return
|
||||
|
||||
await self.send_connect()
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if DISPLAY_MSGS:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
self.locations_array = None
|
||||
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
|
||||
self.set_deathlink = True
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args['seed_name']
|
||||
elif cmd == 'Print':
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
self.deathlink_pending = True
|
||||
super().on_deathlink(data)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class GBManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Pokémon Client"
|
||||
|
||||
self.ui = GBManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
def get_payload(ctx: GBContext):
|
||||
current_time = time.time()
|
||||
ret = json.dumps(
|
||||
{
|
||||
"items": [item.item for item in ctx.items_received],
|
||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||
if key[0] > current_time - 10},
|
||||
"deathlink": ctx.deathlink_pending,
|
||||
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
|
||||
}
|
||||
)
|
||||
ctx.deathlink_pending = False
|
||||
return ret
|
||||
|
||||
|
||||
async def parse_locations(data: List, ctx: GBContext):
|
||||
locations = []
|
||||
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
||||
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
|
||||
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
|
||||
|
||||
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
|
||||
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
|
||||
else:
|
||||
flags["DexSanityFlag"] = [0] * 19
|
||||
|
||||
for flag_type, loc_map in location_map.items():
|
||||
for flag, loc_id in loc_map.items():
|
||||
if flag_type == "list":
|
||||
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
|
||||
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
|
||||
locations.append(loc_id)
|
||||
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
|
||||
locations.append(loc_id)
|
||||
|
||||
hints = []
|
||||
if flags["EventFlag"][280] & 16:
|
||||
hints.append("Cerulean Bicycle Shop")
|
||||
if flags["EventFlag"][280] & 32:
|
||||
hints.append("Route 2 Gate - Oak's Aide")
|
||||
if flags["EventFlag"][280] & 64:
|
||||
hints.append("Route 11 Gate 2F - Oak's Aide")
|
||||
if flags["EventFlag"][280] & 128:
|
||||
hints.append("Route 15 Gate 2F - Oak's Aide")
|
||||
if flags["EventFlag"][281] & 1:
|
||||
hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2",
|
||||
"Celadon Prize Corner - Item Prize 3"]
|
||||
if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id["Fossil - Choice B"]
|
||||
not in ctx.checked_locations):
|
||||
hints.append("Fossil - Choice B")
|
||||
elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"]
|
||||
not in ctx.checked_locations):
|
||||
hints.append("Fossil - Choice A")
|
||||
hints = [
|
||||
location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and
|
||||
location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked
|
||||
]
|
||||
if hints:
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
|
||||
ctx.auto_hints.update(hints)
|
||||
|
||||
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
"status": 30}
|
||||
])
|
||||
ctx.finished_game = True
|
||||
if locations == ctx.locations_array:
|
||||
return
|
||||
ctx.locations_array = locations
|
||||
if locations is not None:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
|
||||
|
||||
|
||||
async def gb_sync_task(ctx: GBContext):
|
||||
logger.info("Starting GB connector. Use /gb for status information")
|
||||
while not ctx.exit_event.is_set():
|
||||
error_status = None
|
||||
if ctx.gb_streams:
|
||||
(reader, writer) = ctx.gb_streams
|
||||
msg = get_payload(ctx).encode()
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
try:
|
||||
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||
try:
|
||||
# Data will return a dict with up to two fields:
|
||||
# 1. A keepalive response of the Players Name (always)
|
||||
# 2. An array representing the memory values of the locations area (if in game)
|
||||
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
data_decoded = json.loads(data.decode())
|
||||
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
|
||||
msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \
|
||||
"and PokemonClient are from the same Archipelago installation."
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion']
|
||||
if ctx.client_compatibility_mode == 0:
|
||||
ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested
|
||||
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
|
||||
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0])
|
||||
if not ctx.auth:
|
||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||
if ctx.auth == '':
|
||||
msg = "Invalid ROM detected. No player name built into the ROM."
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
|
||||
and not error_status and ctx.auth:
|
||||
# Not just a keep alive ping, parse
|
||||
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
|
||||
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
|
||||
if 'options' in data_decoded:
|
||||
msgs = []
|
||||
if data_decoded['options'] & 4 and not ctx.sent_release:
|
||||
ctx.sent_release = True
|
||||
msgs.append({"cmd": "Say", "text": "!release"})
|
||||
if data_decoded['options'] & 8 and not ctx.sent_collect:
|
||||
ctx.sent_collect = True
|
||||
msgs.append({"cmd": "Say", "text": "!collect"})
|
||||
if msgs:
|
||||
await ctx.send_msgs(msgs)
|
||||
if ctx.set_deathlink:
|
||||
await ctx.update_death_link(True)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Read Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.gb_streams = None
|
||||
except ConnectionResetError as e:
|
||||
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.gb_streams = None
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.gb_streams = None
|
||||
except ConnectionResetError:
|
||||
logger.debug("Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.gb_streams = None
|
||||
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
|
||||
if not error_status:
|
||||
logger.info("Successfully Connected to Gameboy")
|
||||
ctx.gb_status = CONNECTION_CONNECTED_STATUS
|
||||
else:
|
||||
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
|
||||
elif error_status:
|
||||
ctx.gb_status = error_status
|
||||
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
|
||||
else:
|
||||
try:
|
||||
logger.debug("Attempting to connect to Gameboy")
|
||||
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
|
||||
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Trying Again")
|
||||
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
logger.debug("Connection Refused, Trying Again")
|
||||
ctx.gb_status = CONNECTION_REFUSED_STATUS
|
||||
continue
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
elif os.path.isfile(auto_start):
|
||||
subprocess.Popen([auto_start, romfile],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def patch_and_run_game(game_version, patch_file, ctx):
|
||||
base_name = os.path.splitext(patch_file)[0]
|
||||
comp_path = base_name + '.gb'
|
||||
if game_version == "blue":
|
||||
delta_patch = BlueDeltaPatch
|
||||
else:
|
||||
delta_patch = RedDeltaPatch
|
||||
|
||||
try:
|
||||
base_rom = delta_patch.get_source_data()
|
||||
except Exception as msg:
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
|
||||
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
||||
with patch_archive.open('delta.bsdiff4', 'r') as stream:
|
||||
patch = stream.read()
|
||||
patched_rom_data = bsdiff4.patch(base_rom, patch)
|
||||
|
||||
with open(comp_path, "wb") as patched_rom_file:
|
||||
patched_rom_file.write(patched_rom_data)
|
||||
|
||||
async_start(run_game(comp_path))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Utils.init_logging("PokemonClient")
|
||||
|
||||
options = Utils.get_options()
|
||||
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('patch_file', default="", type=str, nargs="?",
|
||||
help='Path to an APRED or APBLUE patch file')
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = GBContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
|
||||
|
||||
if args.patch_file:
|
||||
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
|
||||
if ext == "apred":
|
||||
logger.info("APRED file supplied, beginning patching process...")
|
||||
async_start(patch_and_run_game("red", args.patch_file, ctx))
|
||||
elif ext == "apblue":
|
||||
logger.info("APBLUE file supplied, beginning patching process...")
|
||||
async_start(patch_and_run_game("blue", args.patch_file, ctx))
|
||||
else:
|
||||
logger.warning(f"Unknown patch file extension {ext}")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.gb_sync_task:
|
||||
await ctx.gb_sync_task
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
@@ -51,13 +51,6 @@ Currently, the following games are supported:
|
||||
* Muse Dash
|
||||
* DOOM 1993
|
||||
* Terraria
|
||||
* Lingo
|
||||
* Pokémon Emerald
|
||||
* DOOM II
|
||||
* Shivers
|
||||
* Heretic
|
||||
* Landstalker: The Treasures of King Nole
|
||||
* Final Fantasy Mystic Quest
|
||||
|
||||
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
|
||||
|
||||
@@ -27,14 +27,14 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
self.ctx.syncing = True
|
||||
|
||||
def _cmd_patch(self):
|
||||
"""Patch the game. Only use this command if /auto_patch fails."""
|
||||
"""Patch the game."""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||
self.ctx.patch_game()
|
||||
self.output("Patched.")
|
||||
|
||||
def _cmd_savepath(self, directory: str):
|
||||
"""Redirect to proper save data folder. This is necessary for Linux users to use before connecting."""
|
||||
"""Redirect to proper save data folder. (Use before connecting!)"""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
self.ctx.save_game_folder = directory
|
||||
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
|
||||
@@ -67,7 +67,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
self.output("Patching successful!")
|
||||
|
||||
def _cmd_online(self):
|
||||
"""Toggles seeing other Undertale players."""
|
||||
"""Makes you no longer able to see other Undertale players."""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
self.ctx.update_online_mode(not ("Online" in self.ctx.tags))
|
||||
if "Online" in self.ctx.tags:
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.4"
|
||||
__version__ = "0.4.3"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -113,9 +113,6 @@ class WargrooveContext(CommonContext):
|
||||
async def connection_closed(self):
|
||||
await super(WargrooveContext, self).connection_closed()
|
||||
self.remove_communication_files()
|
||||
self.checked_locations.clear()
|
||||
self.server_locations.clear()
|
||||
self.finished_game = False
|
||||
|
||||
@property
|
||||
def endpoints(self):
|
||||
@@ -127,9 +124,6 @@ class WargrooveContext(CommonContext):
|
||||
async def shutdown(self):
|
||||
await super(WargrooveContext, self).shutdown()
|
||||
self.remove_communication_files()
|
||||
self.checked_locations.clear()
|
||||
self.server_locations.clear()
|
||||
self.finished_game = False
|
||||
|
||||
def remove_communication_files(self):
|
||||
for root, dirs, files in os.walk(self.game_communication_path):
|
||||
@@ -408,10 +402,8 @@ async def game_watcher(ctx: WargrooveContext):
|
||||
if file.find("send") > -1:
|
||||
st = file.split("send", -1)[1]
|
||||
sending = sending+[(int(st))]
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file.find("victory") > -1:
|
||||
victory = True
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
ctx.locations_checked = sending
|
||||
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||
await ctx.send_msgs(message)
|
||||
|
||||
@@ -27,10 +27,8 @@ from .models import Command, GameDataPackage, Room, db
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
ctx: WebHostContext
|
||||
|
||||
def _cmd_video(self, platform: str, user: str):
|
||||
"""Set a link for your name in the WebHostLib tracker pointing to a video stream.
|
||||
Currently, only YouTube and Twitch platforms are supported.
|
||||
"""
|
||||
def _cmd_video(self, platform, user):
|
||||
"""Set a link for your name in the WebHostLib tracker pointing to a video stream"""
|
||||
if platform.lower().startswith("t"): # twitch
|
||||
self.ctx.video[self.client.team, self.client.slot] = "Twitch", user
|
||||
self.ctx.save()
|
||||
|
||||
@@ -90,8 +90,6 @@ def download_slot_file(room_id, player_id: int):
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
||||
elif slot_data.game == "Kingdom Hearts 2":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
|
||||
elif slot_data.game == "Final Fantasy Mystic Quest":
|
||||
fname = f"AP+{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmq"
|
||||
else:
|
||||
return "Game download not supported."
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import tempfile
|
||||
import zipfile
|
||||
import concurrent.futures
|
||||
from collections import Counter
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Dict, Optional, Any, Union, List
|
||||
|
||||
from flask import flash, redirect, render_template, request, session, url_for
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from pony.orm import commit, db_session
|
||||
|
||||
from BaseClasses import get_seed, seeddigits
|
||||
from Generate import PlandoOptions, handle_name
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
from Generate import handle_name, PlandoOptions
|
||||
from Main import main as ERmain
|
||||
from Utils import __version__
|
||||
from WebHostLib import app
|
||||
@@ -131,7 +131,6 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
||||
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
erargs.skip_prog_balancing = False
|
||||
erargs.skip_output = False
|
||||
|
||||
name_counter = Counter()
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
|
||||
@@ -3,8 +3,11 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
|
||||
import Options
|
||||
from Utils import local_path
|
||||
from Utils import __version__, local_path
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
@@ -25,7 +28,7 @@ def create():
|
||||
weighted_options = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "",
|
||||
"name": "Player",
|
||||
"game": {},
|
||||
},
|
||||
"games": {},
|
||||
@@ -40,7 +43,7 @@ def create():
|
||||
"baseOptions": {
|
||||
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||
"game": game_name,
|
||||
"name": "",
|
||||
"name": "Player",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,8 +84,8 @@ def create():
|
||||
"max": option.range_end,
|
||||
}
|
||||
|
||||
if issubclass(option, Options.NamedRange):
|
||||
game_options[option_name]["type"] = 'named_range'
|
||||
if issubclass(option, Options.SpecialRange):
|
||||
game_options[option_name]["type"] = 'special_range'
|
||||
game_options[option_name]["value_names"] = {}
|
||||
for key, val in option.special_range_names.items():
|
||||
game_options[option_name]["value_names"][key] = val
|
||||
@@ -114,46 +117,10 @@ def create():
|
||||
}
|
||||
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
logging.debug(f"{option} not exported to Web options.")
|
||||
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
for option_name, option_value in preset.items():
|
||||
# Random range type settings are not valid.
|
||||
assert (not str(option_value).startswith("random-")), \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
|
||||
f"values are not supported for presets."
|
||||
|
||||
# Normal random is supported, but needs to be handled explicitly.
|
||||
if option_value == "random":
|
||||
player_options["presetOptions"][preset_name][option_name] = option_value
|
||||
continue
|
||||
|
||||
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
if isinstance(option, Options.NamedRange) and isinstance(option_value, str):
|
||||
assert option_value in option.special_range_names, \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
|
||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||
|
||||
# Still use the true value for the option, not the name.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option, Options.Range):
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option_value, str):
|
||||
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
|
||||
# setting a preset for an option with an overridden from_text method that would normally be okay,
|
||||
# but would not be okay for the webhost's current implementation of player options UI.
|
||||
assert option.name_lookup[option.value] == option_value, \
|
||||
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
|
||||
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
else:
|
||||
# int and bool values are fine, just resolve them to the current key for webhost.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
@@ -169,20 +136,16 @@ def create():
|
||||
option["defaultValue"] = "random"
|
||||
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": game_options,
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
],
|
||||
"gameItemDescriptions": world.item_descriptions,
|
||||
"gameLocations": tuple(world.location_names),
|
||||
"gameLocationGroups": [
|
||||
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||
],
|
||||
"gameLocationDescriptions": world.location_descriptions,
|
||||
}
|
||||
weighted_options["games"][game_name] = {}
|
||||
weighted_options["games"][game_name]["gameSettings"] = game_options
|
||||
weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names)
|
||||
weighted_options["games"][game_name]["gameItemGroups"] = [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
]
|
||||
weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names)
|
||||
weighted_options["games"][game_name]["gameLocationGroups"] = [
|
||||
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||
]
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@ window.addEventListener('load', () => {
|
||||
}
|
||||
|
||||
if (optionHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage(
|
||||
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
|
||||
);
|
||||
showUserMessage("Your options are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.");
|
||||
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||
}
|
||||
|
||||
@@ -37,17 +36,6 @@ window.addEventListener('load', () => {
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||
nameInput.value = playerOptions.name;
|
||||
|
||||
// Presets
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
|
||||
for (const preset in results['presetOptions']) {
|
||||
const presetOption = document.createElement('option');
|
||||
presetOption.innerText = preset;
|
||||
presetSelect.appendChild(presetOption);
|
||||
}
|
||||
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
|
||||
results['presetOptions']['__default'] = {};
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
@@ -57,8 +45,7 @@ window.addEventListener('load', () => {
|
||||
|
||||
const resetOptions = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`);
|
||||
localStorage.removeItem(`${gameName}-preset`);
|
||||
localStorage.removeItem(`${gameName}-hash`)
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
@@ -90,10 +77,6 @@ const createDefaultOptions = (optionData) => {
|
||||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||
}
|
||||
|
||||
if (!localStorage.getItem(`${gameName}-preset`)) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__default');
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (optionData) => {
|
||||
@@ -101,11 +84,8 @@ const buildUI = (optionData) => {
|
||||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) {
|
||||
leftGameOpts[key] = optionData.gameOptions[key];
|
||||
} else {
|
||||
rightGameOpts[key] = optionData.gameOptions[key];
|
||||
}
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) { leftGameOpts[key] = optionData.gameOptions[key]; }
|
||||
else { rightGameOpts[key] = optionData.gameOptions[key]; }
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
@@ -140,7 +120,7 @@ const buildOptionsTable = (options, romOpts = false) => {
|
||||
|
||||
const randomButton = document.createElement('button');
|
||||
|
||||
switch(options[option].type) {
|
||||
switch(options[option].type){
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
@@ -149,17 +129,16 @@ const buildOptionsTable = (options, romOpts = false) => {
|
||||
select.setAttribute('data-key', option);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
options[option].options.forEach((opt) => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.setAttribute('value', opt.value);
|
||||
optionElement.innerText = opt.name;
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', opt.value);
|
||||
option.innerText = opt.name;
|
||||
if ((isNaN(currentOptions[gameName][option]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||
(opt.value === currentOptions[gameName][option]))
|
||||
{
|
||||
optionElement.selected = true;
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(optionElement);
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||
element.appendChild(select);
|
||||
@@ -183,7 +162,6 @@ const buildOptionsTable = (options, romOpts = false) => {
|
||||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('id', option);
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', option);
|
||||
range.setAttribute('min', options[option].min);
|
||||
@@ -216,74 +194,74 @@ const buildOptionsTable = (options, romOpts = false) => {
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'named_range':
|
||||
case 'special_range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('named-range-container');
|
||||
element.classList.add('special-range-container');
|
||||
|
||||
// Build the select element
|
||||
let namedRangeSelect = document.createElement('select');
|
||||
namedRangeSelect.setAttribute('data-key', option);
|
||||
let specialRangeSelect = document.createElement('select');
|
||||
specialRangeSelect.setAttribute('data-key', option);
|
||||
Object.keys(options[option].value_names).forEach((presetName) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = options[option].value_names[presetName];
|
||||
const words = presetOption.innerText.split('_');
|
||||
const words = presetOption.innerText.split("_");
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(' ');
|
||||
namedRangeSelect.appendChild(presetOption);
|
||||
presetOption.innerText = words.join(" ");
|
||||
specialRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
customOption.innerText = 'Custom';
|
||||
customOption.value = 'custom';
|
||||
customOption.selected = true;
|
||||
namedRangeSelect.appendChild(customOption);
|
||||
specialRangeSelect.appendChild(customOption);
|
||||
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
|
||||
namedRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||
specialRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||
}
|
||||
|
||||
// Build range element
|
||||
let namedRangeWrapper = document.createElement('div');
|
||||
namedRangeWrapper.classList.add('named-range-wrapper');
|
||||
let namedRange = document.createElement('input');
|
||||
namedRange.setAttribute('type', 'range');
|
||||
namedRange.setAttribute('data-key', option);
|
||||
namedRange.setAttribute('min', options[option].min);
|
||||
namedRange.setAttribute('max', options[option].max);
|
||||
namedRange.value = currentOptions[gameName][option];
|
||||
let specialRangeWrapper = document.createElement('div');
|
||||
specialRangeWrapper.classList.add('special-range-wrapper');
|
||||
let specialRange = document.createElement('input');
|
||||
specialRange.setAttribute('type', 'range');
|
||||
specialRange.setAttribute('data-key', option);
|
||||
specialRange.setAttribute('min', options[option].min);
|
||||
specialRange.setAttribute('max', options[option].max);
|
||||
specialRange.value = currentOptions[gameName][option];
|
||||
|
||||
// Build rage value element
|
||||
let namedRangeVal = document.createElement('span');
|
||||
namedRangeVal.classList.add('range-value');
|
||||
namedRangeVal.setAttribute('id', `${option}-value`);
|
||||
namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
let specialRangeVal = document.createElement('span');
|
||||
specialRangeVal.classList.add('range-value');
|
||||
specialRangeVal.setAttribute('id', `${option}-value`);
|
||||
specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
|
||||
// Configure select event listener
|
||||
namedRangeSelect.addEventListener('change', (event) => {
|
||||
specialRangeSelect.addEventListener('change', (event) => {
|
||||
if (event.target.value === 'custom') { return; }
|
||||
|
||||
// Update range slider
|
||||
namedRange.value = event.target.value;
|
||||
specialRange.value = event.target.value;
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
// Configure range event handler
|
||||
namedRange.addEventListener('change', (event) => {
|
||||
specialRange.addEventListener('change', (event) => {
|
||||
// Update select element
|
||||
namedRangeSelect.value =
|
||||
specialRangeSelect.value =
|
||||
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
|
||||
parseInt(event.target.value) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
element.appendChild(namedRangeSelect);
|
||||
namedRangeWrapper.appendChild(namedRange);
|
||||
namedRangeWrapper.appendChild(namedRangeVal);
|
||||
element.appendChild(namedRangeWrapper);
|
||||
element.appendChild(specialRangeSelect);
|
||||
specialRangeWrapper.appendChild(specialRange);
|
||||
specialRangeWrapper.appendChild(specialRangeVal);
|
||||
element.appendChild(specialRangeWrapper);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
@@ -291,15 +269,15 @@ const buildOptionsTable = (options, romOpts = false) => {
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, namedRange, namedRangeSelect)
|
||||
event, specialRange, specialRangeSelect)
|
||||
);
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
namedRange.disabled = true;
|
||||
namedRangeSelect.disabled = true;
|
||||
specialRange.disabled = true;
|
||||
specialRangeSelect.disabled = true;
|
||||
}
|
||||
|
||||
namedRangeWrapper.appendChild(randomButton);
|
||||
specialRangeWrapper.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -316,90 +294,6 @@ const buildOptionsTable = (options, romOpts = false) => {
|
||||
return table;
|
||||
};
|
||||
|
||||
const setPresets = (optionsData, presetName) => {
|
||||
const defaults = optionsData['gameOptions'];
|
||||
const preset = optionsData['presetOptions'][presetName];
|
||||
|
||||
localStorage.setItem(`${gameName}-preset`, presetName);
|
||||
|
||||
if (!preset) {
|
||||
console.error(`No presets defined for preset name: '${presetName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateOptionElement = (option, presetValue) => {
|
||||
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
optionElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
optionElement.value = presetValue;
|
||||
randomElement.classList.remove('active');
|
||||
optionElement.disabled = undefined;
|
||||
updateGameOption(optionElement, false);
|
||||
}
|
||||
};
|
||||
|
||||
for (const option in defaults) {
|
||||
let presetValue = preset[option];
|
||||
if (presetValue === undefined) {
|
||||
// Using the default value if not set in presets.
|
||||
presetValue = defaults[option]['defaultValue'];
|
||||
}
|
||||
|
||||
switch (defaults[option].type) {
|
||||
case 'range':
|
||||
const numberElement = document.querySelector(`#${option}-value`);
|
||||
if (presetValue === 'random') {
|
||||
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
|
||||
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
|
||||
: defaults[option]['defaultValue'];
|
||||
} else {
|
||||
numberElement.innerText = presetValue;
|
||||
}
|
||||
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
|
||||
case 'select': {
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'special_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
selectElement.disabled = true;
|
||||
rangeElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
rangeElement.value = presetValue;
|
||||
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
|
||||
parseInt(presetValue) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = presetValue;
|
||||
|
||||
randomElement.classList.remove('active');
|
||||
selectElement.disabled = undefined;
|
||||
rangeElement.disabled = undefined;
|
||||
updateGameOption(rangeElement, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
@@ -427,15 +321,8 @@ const updateBaseOption = (event) => {
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||
const updateGameOption = (optionElement) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
|
||||
if (toggleCustomPreset) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__custom');
|
||||
const presetElement = document.getElementById('game-options-preset');
|
||||
presetElement.value = '__custom';
|
||||
}
|
||||
|
||||
if (optionElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||
@@ -449,21 +336,7 @@ const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||
|
||||
const exportOptions = () => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
const preset = localStorage.getItem(`${gameName}-preset`);
|
||||
switch (preset) {
|
||||
case '__default':
|
||||
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
|
||||
break;
|
||||
|
||||
case '__custom':
|
||||
options['description'] = `Generated by https://archipelago.gg.`;
|
||||
break;
|
||||
|
||||
default:
|
||||
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
|
||||
}
|
||||
|
||||
if (!options.name || options.name.toString().trim().length === 0) {
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
|
||||
@@ -4,20 +4,13 @@ const adjustTableHeight = () => {
|
||||
return;
|
||||
const upperDistance = tablesContainer.getBoundingClientRect().top;
|
||||
|
||||
const containerHeight = window.innerHeight - upperDistance;
|
||||
tablesContainer.style.maxHeight = `calc(${containerHeight}px - 1rem)`;
|
||||
|
||||
const tableWrappers = document.getElementsByClassName('table-wrapper');
|
||||
for (let i = 0; i < tableWrappers.length; i++) {
|
||||
// Ensure we are starting from maximum size prior to calculation.
|
||||
tableWrappers[i].style.height = null;
|
||||
tableWrappers[i].style.maxHeight = null;
|
||||
|
||||
// Set as a reasonable height, but still allows the user to resize element if they desire.
|
||||
const currentHeight = tableWrappers[i].offsetHeight;
|
||||
const maxHeight = (window.innerHeight - upperDistance) / Math.min(tableWrappers.length, 4);
|
||||
if (currentHeight > maxHeight) {
|
||||
tableWrappers[i].style.height = `calc(${maxHeight}px - 1rem)`;
|
||||
}
|
||||
|
||||
tableWrappers[i].style.maxHeight = `${currentHeight}px`;
|
||||
for(let i=0; i < tableWrappers.length; i++){
|
||||
const maxHeight = (window.innerHeight - upperDistance) / 2;
|
||||
tableWrappers[i].style.maxHeight = `calc(${maxHeight}px - 1rem)`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,7 +55,7 @@ window.addEventListener('load', () => {
|
||||
render: function (data, type, row) {
|
||||
if (type === "sort" || type === 'type') {
|
||||
if (data === "None")
|
||||
return Number.MAX_VALUE;
|
||||
return -1;
|
||||
|
||||
return parseInt(data);
|
||||
}
|
||||
|
||||
@@ -93,10 +93,9 @@ class WeightedSettings {
|
||||
});
|
||||
break;
|
||||
case 'range':
|
||||
case 'named_range':
|
||||
case 'special_range':
|
||||
this.current[game][gameSetting]['random'] = 0;
|
||||
this.current[game][gameSetting]['random-low'] = 0;
|
||||
this.current[game][gameSetting]['random-middle'] = 0;
|
||||
this.current[game][gameSetting]['random-high'] = 0;
|
||||
if (setting.hasOwnProperty('defaultValue')) {
|
||||
this.current[game][gameSetting][setting.defaultValue] = 25;
|
||||
@@ -211,11 +210,7 @@ class WeightedSettings {
|
||||
let errorMessage = null;
|
||||
|
||||
// User must choose a name for their file
|
||||
if (
|
||||
!settings.name ||
|
||||
settings.name.toString().trim().length === 0 ||
|
||||
settings.name.toString().toLowerCase().trim() === 'player'
|
||||
) {
|
||||
if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') {
|
||||
userMessage.innerText = 'You forgot to set your player name at the top of the page!';
|
||||
userMessage.classList.add('visible');
|
||||
userMessage.scrollIntoView({
|
||||
@@ -261,7 +256,7 @@ class WeightedSettings {
|
||||
|
||||
// Remove empty arrays
|
||||
else if (
|
||||
['exclude_locations', 'priority_locations', 'local_items',
|
||||
['exclude_locations', 'priority_locations', 'local_items',
|
||||
'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) &&
|
||||
settings[game][setting].length === 0
|
||||
) {
|
||||
@@ -523,185 +518,178 @@ class GameSettings {
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
case 'named_range':
|
||||
case 'special_range':
|
||||
const rangeTable = document.createElement('table');
|
||||
const rangeTbody = document.createElement('tbody');
|
||||
|
||||
const hintText = document.createElement('p');
|
||||
hintText.classList.add('hint-text');
|
||||
hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
|
||||
`below, then press the "Add" button to add a weight for it.<br /><br />Accepted values:<br />` +
|
||||
`Normal range: ${setting.min} - ${setting.max}`;
|
||||
if (((setting.max - setting.min) + 1) < 11) {
|
||||
for (let i=setting.min; i <= setting.max; ++i) {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = i;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const acceptedValuesOutsideRange = [];
|
||||
if (setting.hasOwnProperty('value_names')) {
|
||||
Object.keys(setting.value_names).forEach((specialName) => {
|
||||
if (
|
||||
(setting.value_names[specialName] < setting.min) ||
|
||||
(setting.value_names[specialName] > setting.max)
|
||||
) {
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${this.name}-${settingName}-${i}-range`);
|
||||
range.setAttribute('data-game', this.name);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', i);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
|
||||
range.value = this.current[settingName][i] || 0;
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${this.name}-${settingName}-${i}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
}
|
||||
} else {
|
||||
const hintText = document.createElement('p');
|
||||
hintText.classList.add('hint-text');
|
||||
hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
|
||||
`below, then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
|
||||
`Maximum value: ${setting.max}`;
|
||||
|
||||
if (setting.hasOwnProperty('value_names')) {
|
||||
hintText.innerHTML += '<br /><br />Certain values have special meaning:';
|
||||
Object.keys(setting.value_names).forEach((specialName) => {
|
||||
hintText.innerHTML += `<br />${specialName}: ${setting.value_names[specialName]}`;
|
||||
acceptedValuesOutsideRange.push(setting.value_names[specialName]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
settingWrapper.appendChild(hintText);
|
||||
|
||||
const addOptionDiv = document.createElement('div');
|
||||
addOptionDiv.classList.add('add-option-div');
|
||||
const optionInput = document.createElement('input');
|
||||
optionInput.setAttribute('id', `${this.name}-${settingName}-option`);
|
||||
optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
|
||||
addOptionDiv.appendChild(optionInput);
|
||||
const addOptionButton = document.createElement('button');
|
||||
addOptionButton.innerText = 'Add';
|
||||
addOptionDiv.appendChild(addOptionButton);
|
||||
settingWrapper.appendChild(addOptionDiv);
|
||||
optionInput.addEventListener('keydown', (evt) => {
|
||||
if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
|
||||
});
|
||||
|
||||
hintText.innerHTML += '<br /><br />Certain values have special meaning:';
|
||||
Object.keys(setting.value_names).forEach((specialName) => {
|
||||
hintText.innerHTML += `<br />${specialName}: ${setting.value_names[specialName]}`;
|
||||
addOptionButton.addEventListener('click', () => {
|
||||
const optionInput = document.getElementById(`${this.name}-${settingName}-option`);
|
||||
let option = optionInput.value;
|
||||
if (!option || !option.trim()) { return; }
|
||||
option = parseInt(option, 10);
|
||||
if ((option < setting.min) || (option > setting.max)) { return; }
|
||||
optionInput.value = '';
|
||||
if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', this.name);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
|
||||
range.value = this.current[settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
const tdDelete = document.createElement('td');
|
||||
tdDelete.classList.add('td-delete');
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('range-option-delete');
|
||||
deleteButton.innerText = '❌';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
range.value = 0;
|
||||
range.dispatchEvent(new Event('change'));
|
||||
rangeTbody.removeChild(tr);
|
||||
});
|
||||
tdDelete.appendChild(deleteButton);
|
||||
tr.appendChild(tdDelete);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
|
||||
// Save new option to settings
|
||||
range.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
Object.keys(this.current[settingName]).forEach((option) => {
|
||||
// These options are statically generated below, and should always appear even if they are deleted
|
||||
// from localStorage
|
||||
if (['random-low', 'random', 'random-high'].includes(option)) { return; }
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', this.name);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
|
||||
range.value = this.current[settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
const tdDelete = document.createElement('td');
|
||||
tdDelete.classList.add('td-delete');
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('range-option-delete');
|
||||
deleteButton.innerText = '❌';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
range.value = 0;
|
||||
const changeEvent = new Event('change');
|
||||
changeEvent.action = 'rangeDelete';
|
||||
range.dispatchEvent(changeEvent);
|
||||
rangeTbody.removeChild(tr);
|
||||
});
|
||||
tdDelete.appendChild(deleteButton);
|
||||
tr.appendChild(tdDelete);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
settingWrapper.appendChild(hintText);
|
||||
|
||||
const addOptionDiv = document.createElement('div');
|
||||
addOptionDiv.classList.add('add-option-div');
|
||||
const optionInput = document.createElement('input');
|
||||
optionInput.setAttribute('id', `${this.name}-${settingName}-option`);
|
||||
let placeholderText = `${setting.min} - ${setting.max}`;
|
||||
acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`);
|
||||
optionInput.setAttribute('placeholder', placeholderText);
|
||||
addOptionDiv.appendChild(optionInput);
|
||||
const addOptionButton = document.createElement('button');
|
||||
addOptionButton.innerText = 'Add';
|
||||
addOptionDiv.appendChild(addOptionButton);
|
||||
settingWrapper.appendChild(addOptionDiv);
|
||||
optionInput.addEventListener('keydown', (evt) => {
|
||||
if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
|
||||
});
|
||||
|
||||
addOptionButton.addEventListener('click', () => {
|
||||
const optionInput = document.getElementById(`${this.name}-${settingName}-option`);
|
||||
let option = optionInput.value;
|
||||
if (!option || !option.trim()) { return; }
|
||||
option = parseInt(option, 10);
|
||||
|
||||
let optionAcceptable = false;
|
||||
if ((option > setting.min) && (option < setting.max)) {
|
||||
optionAcceptable = true;
|
||||
}
|
||||
if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){
|
||||
optionAcceptable = true;
|
||||
}
|
||||
if (!optionAcceptable) { return; }
|
||||
|
||||
optionInput.value = '';
|
||||
if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
if (
|
||||
setting.hasOwnProperty('value_names') &&
|
||||
Object.values(setting.value_names).includes(parseInt(option, 10))
|
||||
) {
|
||||
const optionName = Object.keys(setting.value_names).find(
|
||||
(key) => setting.value_names[key] === parseInt(option, 10)
|
||||
);
|
||||
tdLeft.innerText += ` [${optionName}]`;
|
||||
}
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', this.name);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
|
||||
range.value = this.current[settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
const tdDelete = document.createElement('td');
|
||||
tdDelete.classList.add('td-delete');
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('range-option-delete');
|
||||
deleteButton.innerText = '❌';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
range.value = 0;
|
||||
range.dispatchEvent(new Event('change'));
|
||||
rangeTbody.removeChild(tr);
|
||||
});
|
||||
tdDelete.appendChild(deleteButton);
|
||||
tr.appendChild(tdDelete);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
|
||||
// Save new option to settings
|
||||
range.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
Object.keys(this.current[settingName]).forEach((option) => {
|
||||
// These options are statically generated below, and should always appear even if they are deleted
|
||||
// from localStorage
|
||||
if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; }
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
if (
|
||||
setting.hasOwnProperty('value_names') &&
|
||||
Object.values(setting.value_names).includes(parseInt(option, 10))
|
||||
) {
|
||||
const optionName = Object.keys(setting.value_names).find(
|
||||
(key) => setting.value_names[key] === parseInt(option, 10)
|
||||
);
|
||||
tdLeft.innerText += ` [${optionName}]`;
|
||||
}
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', this.name);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
|
||||
range.value = this.current[settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
const tdDelete = document.createElement('td');
|
||||
tdDelete.classList.add('td-delete');
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('range-option-delete');
|
||||
deleteButton.innerText = '❌';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
range.value = 0;
|
||||
const changeEvent = new Event('change');
|
||||
changeEvent.action = 'rangeDelete';
|
||||
range.dispatchEvent(changeEvent);
|
||||
rangeTbody.removeChild(tr);
|
||||
});
|
||||
tdDelete.appendChild(deleteButton);
|
||||
tr.appendChild(tdDelete);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => {
|
||||
['random', 'random-low', 'random-high'].forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
@@ -712,9 +700,6 @@ class GameSettings {
|
||||
case 'random-low':
|
||||
tdLeft.innerText = "Random (Low)";
|
||||
break;
|
||||
case 'random-middle':
|
||||
tdLeft.innerText = 'Random (Middle)';
|
||||
break;
|
||||
case 'random-high':
|
||||
tdLeft.innerText = "Random (High)";
|
||||
break;
|
||||
@@ -1039,18 +1024,12 @@ class GameSettings {
|
||||
|
||||
// Builds a div for a setting whose value is a list of locations.
|
||||
#buildLocationsDiv(setting) {
|
||||
return this.#buildListDiv(setting, this.data.gameLocations, {
|
||||
groups: this.data.gameLocationGroups,
|
||||
descriptions: this.data.gameLocationDescriptions,
|
||||
});
|
||||
return this.#buildListDiv(setting, this.data.gameLocations, this.data.gameLocationGroups);
|
||||
}
|
||||
|
||||
// Builds a div for a setting whose value is a list of items.
|
||||
#buildItemsDiv(setting) {
|
||||
return this.#buildListDiv(setting, this.data.gameItems, {
|
||||
groups: this.data.gameItemGroups,
|
||||
descriptions: this.data.gameItemDescriptions
|
||||
});
|
||||
return this.#buildListDiv(setting, this.data.gameItems, this.data.gameItemGroups);
|
||||
}
|
||||
|
||||
// Builds a div for a setting named `setting` with a list value that can
|
||||
@@ -1059,15 +1038,12 @@ class GameSettings {
|
||||
// The `groups` option can be a list of additional options for this list
|
||||
// (usually `item_name_groups` or `location_name_groups`) that are displayed
|
||||
// in a special section at the top of the list.
|
||||
//
|
||||
// The `descriptions` option can be a map from item names or group names to
|
||||
// descriptions for the user's benefit.
|
||||
#buildListDiv(setting, items, {groups = [], descriptions = {}} = {}) {
|
||||
#buildListDiv(setting, items, groups = []) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('simple-list');
|
||||
|
||||
groups.forEach((group) => {
|
||||
const row = this.#addListRow(setting, group, descriptions[group]);
|
||||
const row = this.#addListRow(setting, group);
|
||||
div.appendChild(row);
|
||||
});
|
||||
|
||||
@@ -1076,7 +1052,7 @@ class GameSettings {
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const row = this.#addListRow(setting, item, descriptions[item]);
|
||||
const row = this.#addListRow(setting, item);
|
||||
div.appendChild(row);
|
||||
});
|
||||
|
||||
@@ -1084,9 +1060,7 @@ class GameSettings {
|
||||
}
|
||||
|
||||
// Builds and returns a row for a list of checkboxes.
|
||||
//
|
||||
// If `help` is passed, it's displayed as a help tooltip for this list item.
|
||||
#addListRow(setting, item, help = undefined) {
|
||||
#addListRow(setting, item) {
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('list-row');
|
||||
|
||||
@@ -1107,23 +1081,6 @@ class GameSettings {
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.innerText = item;
|
||||
|
||||
if (help) {
|
||||
const helpSpan = document.createElement('span');
|
||||
helpSpan.classList.add('interactive');
|
||||
helpSpan.setAttribute('data-tooltip', help);
|
||||
helpSpan.innerText = '(?)';
|
||||
name.innerText += ' ';
|
||||
name.appendChild(helpSpan);
|
||||
|
||||
// Put the first 7 tooltips below their rows. CSS tooltips in scrolling
|
||||
// containers can't be visible outside those containers, so this helps
|
||||
// ensure they won't be pushed out the top.
|
||||
if (helpSpan.parentNode.childNodes.length < 7) {
|
||||
helpSpan.classList.add('tooltip-bottom');
|
||||
}
|
||||
}
|
||||
|
||||
label.appendChild(name);
|
||||
|
||||
row.appendChild(label);
|
||||
|
||||
@@ -90,31 +90,6 @@ html{
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#player-options div {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options label {
|
||||
display: inline-block;
|
||||
min-width: 180px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options input,
|
||||
#player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
min-width: 150px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#player-options .left, #player-options .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -160,18 +135,18 @@ html{
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
#player-options table .named-range-container{
|
||||
#player-options table .special-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#player-options table .named-range-wrapper{
|
||||
#player-options table .special-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#player-options table .named-range-wrapper input[type=range]{
|
||||
#player-options table .special-range-wrapper input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -213,12 +188,6 @@ html{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#player-options #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -7,55 +7,138 @@
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
#tracker-wrapper a {
|
||||
#tracker-wrapper a{
|
||||
color: #234ae4;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tracker-header-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
line-height: 20px;
|
||||
gap: 0.5rem;
|
||||
.table-wrapper{
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#tracker-header-bar .info {
|
||||
#tracker-header-bar{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#tracker-header-bar .info{
|
||||
color: #ffffff;
|
||||
padding: 2px;
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
#search{
|
||||
border: 1px solid #000000;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
width: 200px;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#multi-stream-link{
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper.no-footer .dataTables_scrollBody{
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.dataTable{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table.dataTable thead{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody, table.dataTable tfoot{
|
||||
background-color: #dce2bd;
|
||||
font-family: LexendDeca-Light, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
|
||||
background-color: #e2eabb;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td{
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer{
|
||||
border-left: 1px solid #bba967;
|
||||
width: calc(100% - 2px) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
table.dataTable thead th{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.upper-row{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 36px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 22px;
|
||||
top: 46px;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td{
|
||||
border: 1px solid #bba967;
|
||||
}
|
||||
|
||||
table.dataTable tfoot td{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody{
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
table.dataTable .center-column{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.alttp-sprite {
|
||||
height: auto;
|
||||
max-height: 32px;
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
.item-acquired{
|
||||
background-color: #d3c97d;
|
||||
}
|
||||
|
||||
#tracker-navigation {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0.5rem 0.5rem 0.5rem;
|
||||
user-select: none;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.tracker-navigation-bar {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
background-color: #b0a77d;
|
||||
margin: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tracker-navigation-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: block;
|
||||
margin: 4px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: black !important;
|
||||
color: #000;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
@@ -67,100 +150,6 @@
|
||||
background-color: rgb(220, 226, 189);
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#search {
|
||||
border: 1px solid #000000;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper.no-footer .dataTables_scrollBody {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table.dataTable thead {
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody, table.dataTable tfoot {
|
||||
background-color: #dce2bd;
|
||||
font-family: LexendDeca-Light, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover {
|
||||
background-color: #e2eabb;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer {
|
||||
border-left: 1px solid #bba967;
|
||||
width: calc(100% - 2px) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
table.dataTable thead th {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.upper-row {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 36px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 22px;
|
||||
top: 46px;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td {
|
||||
border: 1px solid #bba967;
|
||||
}
|
||||
|
||||
table.dataTable tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
table.dataTable .center-column {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.icon-sprite {
|
||||
height: auto;
|
||||
max-height: 32px;
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
.item-acquired {
|
||||
background-color: #d3c97d;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1700px) {
|
||||
table.dataTable thead th.upper-row{
|
||||
position: -webkit-sticky;
|
||||
@@ -170,7 +159,7 @@ img.icon-sprite {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row {
|
||||
table.dataTable thead th.lower-row{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
@@ -178,11 +167,11 @@ img.icon-sprite {
|
||||
top: 37px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer {
|
||||
table.dataTable, table.dataTable.no-footer{
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
img.icon-sprite {
|
||||
img.alttp-sprite {
|
||||
height: auto;
|
||||
max-height: 24px;
|
||||
min-height: 10px;
|
||||
@@ -198,7 +187,7 @@ img.icon-sprite {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row {
|
||||
table.dataTable thead th.lower-row{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
@@ -206,11 +195,11 @@ img.icon-sprite {
|
||||
top: 32px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer {
|
||||
table.dataTable, table.dataTable.no-footer{
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
img.icon-sprite {
|
||||
img.alttp-sprite {
|
||||
height: auto;
|
||||
max-height: 20px;
|
||||
min-height: 10px;
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr class="column-headers">
|
||||
@@ -1,57 +1,36 @@
|
||||
{% extends "tablepage.html" %}
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/trackerCommon.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include "header/dirtHeader.html" %}
|
||||
|
||||
<div id="tracker-navigation">
|
||||
<div class="tracker-navigation-bar">
|
||||
<a
|
||||
class="tracker-navigation-button"
|
||||
href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}"
|
||||
>
|
||||
🡸 Return to Multiworld Tracker
|
||||
</a>
|
||||
{% if game_specific_tracker %}
|
||||
<a
|
||||
class="tracker-navigation-button"
|
||||
href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}"
|
||||
>
|
||||
Game-Specific Tracker
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search" />
|
||||
<div class="info">This tracker will automatically update itself periodically.</div>
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span class="info">This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
<div class="table-wrapper">
|
||||
<table id="received-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Amount</th>
|
||||
<th>Last Order Received</th>
|
||||
<th>Order Received</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for id, count in inventory.items() if count > 0 %}
|
||||
<tr>
|
||||
<td>{{ item_id_to_name[game][id] }}</td>
|
||||
<td>{{ count }}</td>
|
||||
<td>{{ received_items[id] }}</td>
|
||||
</tr>
|
||||
{% for id, count in inventory.items() %}
|
||||
<tr>
|
||||
<td>{{ id | item_name }}</td>
|
||||
<td>{{ count }}</td>
|
||||
<td>{{received_items[id]}}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
|
||||
</tbody>
|
||||
@@ -60,62 +39,24 @@
|
||||
<div class="table-wrapper">
|
||||
<table id="locations-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th class="center-column">Checked</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>Checked</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{%- for location in locations -%}
|
||||
{% for name in checked_locations %}
|
||||
<tr>
|
||||
<td>{{ location_id_to_name[game][location] }}</td>
|
||||
<td class="center-column">
|
||||
{% if location in checked_locations %}✔{% endif %}
|
||||
</td>
|
||||
<td>{{ name | location_name}}</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
{%- endfor -%}
|
||||
{% for name in not_checked_locations %}
|
||||
<tr>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Game</th>
|
||||
<th>Entrance</th>
|
||||
<th class="center-column">Found</th>
|
||||
<td>{{ name | location_name}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
<tr>
|
||||
<td>
|
||||
{% if hint.finding_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if hint.receiving_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td class="center-column">{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
28
WebHostLib/templates/hintTable.html
Normal file
28
WebHostLib/templates/hintTable.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% for team, hints in hints.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Entrance</th>
|
||||
<th>Found</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
<tr>
|
||||
<td>{{ long_player_names[team, hint.finding_player] }}</td>
|
||||
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
|
||||
<td>{{ hint.item|item_name }}</td>
|
||||
<td>{{ hint.location|location_name }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td>{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
171
WebHostLib/templates/lttpMultiTracker.html
Normal file
171
WebHostLib/templates/lttpMultiTracker.html
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>ALttP Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttpMultiTracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
{% include 'multiTrackerNavigation.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
|
||||
<a target="_blank" href="https://multistream.me/
|
||||
{%- for platform, link in video.values()|unique(False, 1)-%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}">
|
||||
Multistream
|
||||
</a>
|
||||
</span>
|
||||
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
{% for team, players in inventory.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="inventory-table" class="table unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
{%- for name in tracking_names -%}
|
||||
{%- if name in icons -%}
|
||||
<th class="center-column">
|
||||
<img class="alttp-sprite" src="{{ icons[name] }}" alt="{{ name|e }}">
|
||||
</th>
|
||||
{%- else -%}
|
||||
<th class="center-column">{{ name|e }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, items in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
{%- if (team, loop.index) in video -%}
|
||||
{%- if video[(team, loop.index)][0] == "Twitch" -%}
|
||||
<td>
|
||||
<a target="_blank" href="https://www.twitch.tv/{{ video[(team, loop.index)][1] }}">
|
||||
{{ player_names[(team, loop.index)] }}
|
||||
▶️</a></td>
|
||||
{%- elif video[(team, loop.index)][0] == "Youtube" -%}
|
||||
<td>
|
||||
<a target="_blank" href="youtube.com/c/{{ video[(team, loop.index)][1] }}/live">
|
||||
{{ player_names[(team, loop.index)] }}
|
||||
▶️</a></td>
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
<td>{{ player_names[(team, loop.index)] }}</td>
|
||||
{%- endif -%}
|
||||
{%- for id in tracking_ids -%}
|
||||
{%- if items[id] -%}
|
||||
<td class="center-column item-acquired">
|
||||
{% if id in multi_items %}{{ items[id] }}{% else %}✔️{% endif %}</td>
|
||||
{%- else -%}
|
||||
<td></td>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for team, players in checks_done.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for area in ordered_areas %}
|
||||
{% set colspan = 1 %}
|
||||
{% if area in key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in icons %}
|
||||
<th colspan="{{ colspan }}" class="center-column upper-row">
|
||||
<img class="alttp-sprite" src="{{ icons[area] }}" alt="{{ area }}"></th>
|
||||
{%- else -%}
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<th rowspan="2" class="center-column">%</th>
|
||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for area in ordered_areas %}
|
||||
<th class="center-column lower-row fraction">
|
||||
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
||||
</th>
|
||||
{% if area in key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
|
||||
</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{% if player in checks_in_area and area in checks_in_area[player] %}
|
||||
{%- set checks_done = checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[player][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
<td class="center-column">{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% include "hintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -50,9 +50,6 @@
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download JSON File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
{% else %}
|
||||
No file to download for this game.
|
||||
{% endif %}
|
||||
|
||||
@@ -8,18 +8,13 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ tools_url }}" class="{{ 'acquired' }}" title="Progressive Tools" /></td>
|
||||
<td><img src="{{ weapons_url }}" class="{{ 'acquired' }}" title="Progressive Weapons" /></td>
|
||||
<td><img src="{{ armor_url }}" class="{{ 'acquired' }}" title="Progressive Armor" /></td>
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
title="Progressive Resource Crafting" /></td>
|
||||
<td><img src="{{ icons['Brewing Stand'] }}" class="{{ 'acquired' if 'Brewing' in acquired_items }}" title="Brewing" /></td>
|
||||
<td>
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "multitracker.html" %}
|
||||
{% extends "multiTracker.html" %}
|
||||
{# establish the to be tracked data. Display Name, factorio/AP internal name, display image #}
|
||||
{%- set science_packs = [
|
||||
("Logistic Science Pack", "logistic-science-pack",
|
||||
@@ -14,12 +14,12 @@
|
||||
("Space Science Pack", "space-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"),
|
||||
] -%}
|
||||
|
||||
{%- block custom_table_headers %}
|
||||
{#- macro that creates a table header with display name and image -#}
|
||||
{%- macro make_header(name, img_src) %}
|
||||
<th class="center-column">
|
||||
<img class="icon-sprite" src="{{ img_src }}" alt="{{ name }}" title="{{ name }}" />
|
||||
<img src="{{ img_src}}"
|
||||
alt="{{ name }}">
|
||||
</th>
|
||||
{% endmacro -%}
|
||||
{#- call the macro to build the table header -#}
|
||||
@@ -27,15 +27,16 @@
|
||||
{{ make_header(name, img_src) }}
|
||||
{% endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_table_row scoped %}
|
||||
{%- set player_inventory = inventories[(team, player)] -%}
|
||||
{% if games[player] == "Factorio" %}
|
||||
{%- set player_inventory = named_inventory[team][player] -%}
|
||||
{%- set prog_science = player_inventory["progressive-science-pack"] -%}
|
||||
{%- for name, internal_name, img_src in science_packs %}
|
||||
{% if player_inventory[internal_name] or prog_science > loop.index0 %}
|
||||
<td class="center-column item-acquired">✔️</td>
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{% endif %}
|
||||
<td class="center-column">{% if player_inventory[internal_name] or prog_science > loop.index0 %}✔{% endif %}</td>
|
||||
{% endfor -%}
|
||||
{% else %}
|
||||
{%- for _ in science_packs %}
|
||||
<td class="center-column">❌</td>
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
{% endblock%}
|
||||
92
WebHostLib/templates/multiTracker.html
Normal file
92
WebHostLib/templates/multiTracker.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
{% include 'multiTrackerNavigation.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
|
||||
<a target="_blank" href="https://multistream.me/
|
||||
{%- for platform, link in video.values()|unique(False, 1)-%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}">
|
||||
Multistream
|
||||
</a>
|
||||
</span>
|
||||
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
{% for team, players in checks_done.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Game</th>
|
||||
<th>Status</th>
|
||||
{% block custom_table_headers %}
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column hours last-activity">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
<td>{{ games[player] }}</td>
|
||||
<td>{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
|
||||
30: "Goal Completed"}.get(states[team, player], "Unknown State") }}</td>
|
||||
{% block custom_table_row scoped %}
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<td class="center-column" data-sort="{{ checks["Total"] }}">
|
||||
{{ checks["Total"] }}/{{ locations[player] | length }}
|
||||
</td>
|
||||
<td class="center-column">{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
{% if not self.custom_table_headers() | trim %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>Total</td>
|
||||
<td>All Games</td>
|
||||
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
|
||||
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
|
||||
<td class="center-column">
|
||||
{% if total_locations[team] == 0 %}
|
||||
100
|
||||
{% else %}
|
||||
{{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="center-column last-activity"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% include "hintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
WebHostLib/templates/multiTrackerNavigation.html
Normal file
9
WebHostLib/templates/multiTrackerNavigation.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||
<div id="tracker-navigation">
|
||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
@@ -1,144 +0,0 @@
|
||||
{% extends "tablepage.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="styles/tracker.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include "header/dirtHeader.html" %}
|
||||
{% include "multitrackerNavigation.html" %}
|
||||
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search" />
|
||||
|
||||
<div
|
||||
id="multi-stream-link"
|
||||
class="tracker-navigation-bar"
|
||||
{% if not videos %}style="display: none"{% endif %}
|
||||
>
|
||||
|
||||
<a
|
||||
class="tracker-navigation-button"
|
||||
href="https://multistream.me/
|
||||
{%- for platform, link in videos.values() | unique(False, 1) -%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}"
|
||||
target="_blank"
|
||||
>
|
||||
► Multistream
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Clicking on a slot's number will bring up the slot-specific tracker.
|
||||
This tracker will automatically update itself periodically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tables-container">
|
||||
{%- for team, players in room_players.items() -%}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
{% if current_tracker == "Generic" %}<th>Game</th>{% endif %}
|
||||
<th>Status</th>
|
||||
{% block custom_table_headers %}
|
||||
{# Implement this block in game-specific multi-trackers. #}
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column hours last-activity">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player in players -%}
|
||||
{%- if current_tracker == "Generic" or games[(team, player)] == current_tracker -%}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">
|
||||
{{ player }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
|
||||
{%- if current_tracker == "Generic" -%}
|
||||
<td>{{ games[(team, player)] }}</td>
|
||||
{%- endif -%}
|
||||
<td>
|
||||
{{
|
||||
{
|
||||
0: "Disconnected",
|
||||
5: "Connected",
|
||||
10: "Ready",
|
||||
20: "Playing",
|
||||
30: "Goal Completed"
|
||||
}.get(states[(team, player)], "Unknown State")
|
||||
}}
|
||||
</td>
|
||||
|
||||
{% block custom_table_row scoped %}
|
||||
{# Implement this block in game-specific multi-trackers. #}
|
||||
{% endblock %}
|
||||
|
||||
{% set location_count = locations[(team, player)] | length %}
|
||||
<td class="center-column" data-sort="{{ locations_complete[(team, player)] }}">
|
||||
{{ locations_complete[(team, player)] }}/{{ location_count }}
|
||||
</td>
|
||||
|
||||
<td class="center-column">
|
||||
{%- if locations[(team, player)] | length > 0 -%}
|
||||
{% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
|
||||
{{ "{0:.2f}".format(percentage_of_completion) }}
|
||||
{%- else -%}
|
||||
100.00
|
||||
{%- endif -%}
|
||||
</td>
|
||||
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
|
||||
{%- if not self.custom_table_headers() | trim -%}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2" style="text-align: right">Total</td>
|
||||
<td>All Games</td>
|
||||
<td>{{ completed_worlds[team] }}/{{ players | length }} Complete</td>
|
||||
<td class="center-column">
|
||||
{{ total_team_locations_complete[team] }}/{{ total_team_locations[team] }}
|
||||
</td>
|
||||
<td class="center-column">
|
||||
{%- if total_team_locations[team] == 0 -%}
|
||||
100
|
||||
{%- else -%}
|
||||
{{ "{0:.2f}".format(total_team_locations_complete[team] / total_team_locations[team] * 100) }}
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="center-column last-activity"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{%- endif -%}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{%- endfor -%}
|
||||
|
||||
{% block custom_tables %}
|
||||
{# Implement this block to create custom tables in game-specific multi-trackers. #}
|
||||
{% endblock %}
|
||||
|
||||
{% include "multitrackerHintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,37 +0,0 @@
|
||||
{% for team, hints in hints.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Game</th>
|
||||
<th>Entrance</th>
|
||||
<th class="center-column">Found</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
{%-
|
||||
if current_tracker == "Generic" or (
|
||||
games[(team, hint.finding_player)] == current_tracker or
|
||||
games[(team, hint.receiving_player)] == current_tracker
|
||||
)
|
||||
-%}
|
||||
<tr>
|
||||
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td>
|
||||
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td class="center-column">{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -1,16 +0,0 @@
|
||||
{% if enabled_trackers | length > 1 %}
|
||||
<div id="tracker-navigation">
|
||||
{# Multitracker game navigation. #}
|
||||
<div class="tracker-navigation-bar">
|
||||
{%- for game_tracker in enabled_trackers -%}
|
||||
{%- set tracker_url = url_for("get_multiworld_tracker", tracker=room.tracker, game=game_tracker) -%}
|
||||
<a
|
||||
class="tracker-navigation-button{% if current_tracker == game_tracker %} selected{% endif %}"
|
||||
href="{{ tracker_url }}"
|
||||
>
|
||||
{{ game_tracker }}
|
||||
</a>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,205 +0,0 @@
|
||||
{% extends "multitracker.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/lttpMultiTracker.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{# List all tracker-relevant icons. Format: (Name, Image URL) #}
|
||||
{%- set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": "https://www.zeldadungeon.net/wiki/images/thumb/6/62/MagicPowder-ALttP-Sprite.png/86px-MagicPowder-ALttP-Sprite.png",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
|
||||
} -%}
|
||||
|
||||
{%- block custom_table_headers %}
|
||||
{#- macro that creates a table header with display name and image -#}
|
||||
{%- macro make_header(name, img_src) %}
|
||||
<th class="center-column">
|
||||
<img height="24" src="{{ img_src }}" title="{{ name }}" alt="{{ name }}" />
|
||||
</th>
|
||||
{% endmacro -%}
|
||||
|
||||
{#- call the macro to build the table header -#}
|
||||
{%- for name in tracking_names %}
|
||||
{%- if name in icons -%}
|
||||
<th class="center-column">
|
||||
<img class="icon-sprite" src="{{ icons[name] }}" alt="{{ name | e }}" title="{{ name | e }}" />
|
||||
</th>
|
||||
{%- endif %}
|
||||
{% endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
{# build each row of custom entries #}
|
||||
{% block custom_table_row scoped %}
|
||||
{%- for id in tracking_ids -%}
|
||||
{# {{ checks }}#}
|
||||
{%- if inventories[(team, player)][id] -%}
|
||||
<td class="center-column item-acquired">
|
||||
{% if id in multi_items %}{{ inventories[(team, player)][id] }}{% else %}✔️{% endif %}
|
||||
</td>
|
||||
{%- else -%}
|
||||
<td></td>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_tables %}
|
||||
|
||||
{% for team, _ in total_team_locations.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="area-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for area in ordered_areas %}
|
||||
{% set colspan = 1 %}
|
||||
{% if area in key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in icons %}
|
||||
<th colspan="{{ colspan }}" class="center-column upper-row">
|
||||
<img class="icon-sprite" src="{{ icons[area] }}" alt="{{ area }}" title="{{ area }}"></th>
|
||||
{%- else -%}
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<th rowspan="2" class="center-column">%</th>
|
||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for area in ordered_areas %}
|
||||
<th class="center-column lower-row fraction">
|
||||
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Checks Complete">
|
||||
</th>
|
||||
{% if area in key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Small Key"] }}" alt="Small Key" title="Small Keys">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Big Key"] }}" alt="Big Key" title="Big Keys">
|
||||
</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for (checks_team, player), area_checks in checks_done.items() if games[(team, player)] == current_tracker and team == checks_team -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ player }}</a></td>
|
||||
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{% if (team, player) in checks_in_area and area in checks_in_area[(team, player)] %}
|
||||
{%- set checks_done = area_checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[(team, player)][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventories[(team, player)][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventories[(team, player)][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
<td class="center-column">
|
||||
{% set location_count = locations[(team, player)] | length %}
|
||||
{%- if locations[(team, player)] | length > 0 -%}
|
||||
{% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
|
||||
{{ "{0:.2f}".format(percentage_of_completion) }}
|
||||
{%- else -%}
|
||||
100.00
|
||||
{%- endif -%}
|
||||
</td>
|
||||
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -28,24 +28,10 @@
|
||||
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset">
|
||||
<option value="__default">Defaults</option>
|
||||
<option value="__custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||
</p>
|
||||
|
||||
<h2>Game Options</h2>
|
||||
<div id="game-options">
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
@@ -7,11 +7,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
@@ -7,11 +7,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="inventory-table">
|
||||
<div class="table-row">
|
||||
@@ -56,16 +51,16 @@
|
||||
<div class="C4"><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></div>
|
||||
{% if 'DownloadableItems' in options %}
|
||||
<div class="C5"><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="table-row">
|
||||
{% if 'DownloadableItems' in options %}
|
||||
<div class="C1"><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="C2"><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></div>
|
||||
{% if 'EyeSpy' in options %}
|
||||
<div class="C3"><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="C4"><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></div>
|
||||
<div class="C5"><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></div>
|
||||
</div>
|
||||
@@ -1,154 +0,0 @@
|
||||
{%- set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
|
||||
} -%}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ icons[bow_icon] }}" class="{{ 'acquired' if bow_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons[glove_icon] }}" class="{{ 'acquired' if glove_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons[sword_icon] }}" class="{{ 'acquired' if sword_acquired }}" /></td>
|
||||
<td><img src="{{ icons[shield_icon] }}" class="{{ 'acquired' if shield_acquired }}" /></td>
|
||||
<td><img src="{{ icons[mail_icon] }}" class="acquired" /></td>
|
||||
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<th><img src="{{ icons["Big Key"] }}" /></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for area in sp_areas %}
|
||||
<tr>
|
||||
<td>{{ area }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<td class="counter">
|
||||
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<td>
|
||||
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,185 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
|
||||
<td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
|
||||
<td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
|
||||
<td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
|
||||
<td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
|
||||
<td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
|
||||
<td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
|
||||
<div class="item-count">{{ hookshot_length }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
|
||||
<td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
|
||||
<td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
|
||||
<td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
|
||||
<td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
|
||||
<td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
|
||||
<td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
|
||||
<td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
|
||||
<td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
|
||||
<td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
|
||||
<div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
|
||||
<td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
|
||||
<td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
|
||||
<div class="item-count">{{ wallet_size }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
|
||||
<td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
|
||||
<div class="item-count">Zelda</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
|
||||
<div class="item-count">Epona</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
|
||||
<div class="item-count">Saria</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
|
||||
<div class="item-count">Sun</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
|
||||
<div class="item-count">Time</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
|
||||
<div class="item-count">Storms</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
|
||||
<div class="item-count">{{ token_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
|
||||
<div class="item-count">Min</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
|
||||
<div class="item-count">Bol</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
|
||||
<div class="item-count">Ser</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
|
||||
<div class="item-count">Req</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
|
||||
<div class="item-count">Noc</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
|
||||
<div class="item-count">Pre</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
|
||||
<div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
|
||||
<td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
|
||||
<td class="right-align">Items</td>
|
||||
</tr>
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
|
||||
<td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,6 @@
|
||||
color: "FFFFFF"
|
||||
<TabbedPanel>:
|
||||
tab_width: root.width / app.tab_count
|
||||
<TooltipLabel>:
|
||||
text_size: self.width, None
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
font_size: dp(20)
|
||||
markup: True
|
||||
<SelectableLabel>:
|
||||
canvas.before:
|
||||
Color:
|
||||
@@ -30,6 +24,11 @@
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
text_size: self.width, None
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
font_size: dp(20)
|
||||
markup: True
|
||||
<UILog>:
|
||||
messages: 1000 # amount of messages stored in client logs.
|
||||
cols: 1
|
||||
@@ -45,70 +44,6 @@
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: dp(3)
|
||||
<HintLabel>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
height: self.minimum_height
|
||||
receiving_text: "Receiving Player"
|
||||
item_text: "Item"
|
||||
finding_text: "Finding Player"
|
||||
location_text: "Location"
|
||||
entrance_text: "Entrance"
|
||||
found_text: "Found?"
|
||||
TooltipLabel:
|
||||
id: receiving
|
||||
text: root.receiving_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: item
|
||||
text: root.item_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: finding
|
||||
text: root.finding_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: location
|
||||
text: root.location_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: entrance
|
||||
text: root.entrance_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: found
|
||||
text: root.found_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
<HintLog>:
|
||||
cols: 1
|
||||
viewclass: 'HintLabel'
|
||||
scroll_y: self.height
|
||||
scroll_type: ["content", "bars"]
|
||||
bar_width: dp(12)
|
||||
effect_cls: "ScrollEffect"
|
||||
SelectableRecycleBoxLayout:
|
||||
default_size: None, dp(20)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: dp(3)
|
||||
<ServerLabel>:
|
||||
text: "Server:"
|
||||
size_hint_x: None
|
||||
|
||||
@@ -249,24 +249,6 @@ Response:
|
||||
- `err` (`string`): A description of the problem
|
||||
]]
|
||||
|
||||
local bizhawk_version = client.getversion()
|
||||
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
|
||||
bizhawk_major = tonumber(bizhawk_major)
|
||||
bizhawk_minor = tonumber(bizhawk_minor)
|
||||
if bizhawk_patch == "" then
|
||||
bizhawk_patch = 0
|
||||
else
|
||||
bizhawk_patch = tonumber(bizhawk_patch)
|
||||
end
|
||||
|
||||
local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
|
||||
lua_major = tonumber(lua_major)
|
||||
lua_minor = tonumber(lua_minor)
|
||||
|
||||
if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
|
||||
require("lua_5_3_compat")
|
||||
end
|
||||
|
||||
local base64 = require("base64")
|
||||
local socket = require("socket")
|
||||
local json = require("json")
|
||||
@@ -275,9 +257,7 @@ local json = require("json")
|
||||
-- Will cause lag due to large console output
|
||||
local DEBUG = false
|
||||
|
||||
local SOCKET_PORT_FIRST = 43055
|
||||
local SOCKET_PORT_RANGE_SIZE = 5
|
||||
local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE
|
||||
local SOCKET_PORT = 43055
|
||||
|
||||
local STATE_NOT_CONNECTED = 0
|
||||
local STATE_CONNECTED = 1
|
||||
@@ -297,6 +277,24 @@ local locked = false
|
||||
|
||||
local rom_hash = nil
|
||||
|
||||
local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
|
||||
lua_major = tonumber(lua_major)
|
||||
lua_minor = tonumber(lua_minor)
|
||||
|
||||
if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
|
||||
require("lua_5_3_compat")
|
||||
end
|
||||
|
||||
local bizhawk_version = client.getversion()
|
||||
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
|
||||
bizhawk_major = tonumber(bizhawk_major)
|
||||
bizhawk_minor = tonumber(bizhawk_minor)
|
||||
if bizhawk_patch == "" then
|
||||
bizhawk_patch = 0
|
||||
else
|
||||
bizhawk_patch = tonumber(bizhawk_patch)
|
||||
end
|
||||
|
||||
function queue_push (self, value)
|
||||
self[self.right] = value
|
||||
self.right = self.right + 1
|
||||
@@ -437,7 +435,7 @@ function send_receive ()
|
||||
end
|
||||
|
||||
if message == "VERSION" then
|
||||
client_socket:send(tostring(SCRIPT_VERSION).."\n")
|
||||
local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n")
|
||||
else
|
||||
local res = {}
|
||||
local data = json.decode(message)
|
||||
@@ -465,45 +463,14 @@ function send_receive ()
|
||||
end
|
||||
end
|
||||
|
||||
function initialize_server ()
|
||||
local err
|
||||
local port = SOCKET_PORT_FIRST
|
||||
local res = nil
|
||||
|
||||
server, err = socket.socket.tcp4()
|
||||
while res == nil and port <= SOCKET_PORT_LAST do
|
||||
res, err = server:bind("localhost", port)
|
||||
if res == nil and err ~= "address already in use" then
|
||||
print(err)
|
||||
return
|
||||
end
|
||||
|
||||
if res == nil then
|
||||
port = port + 1
|
||||
end
|
||||
end
|
||||
|
||||
if port > SOCKET_PORT_LAST then
|
||||
print("Too many instances of connector script already running. Exiting.")
|
||||
return
|
||||
end
|
||||
|
||||
res, err = server:listen(0)
|
||||
|
||||
function main ()
|
||||
server, err = socket.bind("localhost", SOCKET_PORT)
|
||||
if err ~= nil then
|
||||
print(err)
|
||||
return
|
||||
end
|
||||
|
||||
server:settimeout(0)
|
||||
end
|
||||
|
||||
function main ()
|
||||
while true do
|
||||
if server == nil then
|
||||
initialize_server()
|
||||
end
|
||||
|
||||
current_time = socket.socket.gettime()
|
||||
timeout_timer = timeout_timer - (current_time - prev_time)
|
||||
message_timer = message_timer - (current_time - prev_time)
|
||||
@@ -515,16 +482,16 @@ function main ()
|
||||
end
|
||||
|
||||
if current_state == STATE_NOT_CONNECTED then
|
||||
if emu.framecount() % 30 == 0 then
|
||||
print("Looking for client...")
|
||||
if emu.framecount() % 60 == 0 then
|
||||
server:settimeout(2)
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
print("Client connected")
|
||||
current_state = STATE_CONNECTED
|
||||
client_socket = client
|
||||
server:close()
|
||||
server = nil
|
||||
client_socket:settimeout(0)
|
||||
else
|
||||
print("No client found. Trying again...")
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -560,27 +527,27 @@ else
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
rom_hash = gameinfo.getromhash()
|
||||
|
||||
print("Waiting for client to connect. This may take longer the more instances of this script you have open at once.\n")
|
||||
print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n")
|
||||
|
||||
local co = coroutine.create(main)
|
||||
function tick ()
|
||||
local status, err = coroutine.resume(co)
|
||||
|
||||
if not status and err ~= "cannot resume dead coroutine" then
|
||||
|
||||
if not status then
|
||||
print("\nERROR: "..err)
|
||||
print("Consider reporting this crash.\n")
|
||||
|
||||
if server ~= nil then
|
||||
server:close()
|
||||
end
|
||||
|
||||
|
||||
co = coroutine.create(main)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Gambatte has a setting which can cause script execution to become
|
||||
-- misaligned, so for GB and GBC we explicitly set the callback on
|
||||
-- vblank instead.
|
||||
@@ -590,7 +557,7 @@ else
|
||||
else
|
||||
event.onframeend(tick)
|
||||
end
|
||||
|
||||
|
||||
while true do
|
||||
emu.frameadvance()
|
||||
end
|
||||
|
||||
224
data/lua/connector_pkmn_rb.lua
Normal file
224
data/lua/connector_pkmn_rb.lua
Normal file
@@ -0,0 +1,224 @@
|
||||
local socket = require("socket")
|
||||
local json = require('json')
|
||||
local math = require('math')
|
||||
require("common")
|
||||
local STATE_OK = "Ok"
|
||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||
local STATE_UNINITIALIZED = "Uninitialized"
|
||||
|
||||
local SCRIPT_VERSION = 3
|
||||
|
||||
local APIndex = 0x1A6E
|
||||
local APDeathLinkAddress = 0x00FD
|
||||
local APItemAddress = 0x00FF
|
||||
local EventFlagAddress = 0x1735
|
||||
local MissableAddress = 0x161A
|
||||
local HiddenItemsAddress = 0x16DE
|
||||
local RodAddress = 0x1716
|
||||
local DexSanityAddress = 0x1A71
|
||||
local InGameAddress = 0x1A84
|
||||
local ClientCompatibilityAddress = 0xFF00
|
||||
|
||||
local ItemsReceived = nil
|
||||
local playerName = nil
|
||||
local seedName = nil
|
||||
|
||||
local deathlink_rec = nil
|
||||
local deathlink_send = false
|
||||
|
||||
local prevstate = ""
|
||||
local curstate = STATE_UNINITIALIZED
|
||||
local gbSocket = nil
|
||||
local frame = 0
|
||||
|
||||
local compat = nil
|
||||
|
||||
local function defineMemoryFunctions()
|
||||
local memDomain = {}
|
||||
local domains = memory.getmemorydomainlist()
|
||||
memDomain["rom"] = function() memory.usememorydomain("ROM") end
|
||||
memDomain["wram"] = function() memory.usememorydomain("WRAM") end
|
||||
return memDomain
|
||||
end
|
||||
|
||||
local memDomain = defineMemoryFunctions()
|
||||
u8 = memory.read_u8
|
||||
wU8 = memory.write_u8
|
||||
u16 = memory.read_u16_le
|
||||
function uRange(address, bytes)
|
||||
data = memory.readbyterange(address - 1, bytes + 1)
|
||||
data[0] = nil
|
||||
return data
|
||||
end
|
||||
|
||||
function generateLocationsChecked()
|
||||
memDomain.wram()
|
||||
events = uRange(EventFlagAddress, 0x140)
|
||||
missables = uRange(MissableAddress, 0x20)
|
||||
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
|
||||
rod = {u8(RodAddress)}
|
||||
dexsanity = uRange(DexSanityAddress, 19)
|
||||
|
||||
|
||||
data = {}
|
||||
|
||||
categories = {events, missables, hiddenitems, rod}
|
||||
if compat > 1 then
|
||||
table.insert(categories, dexsanity)
|
||||
end
|
||||
for _, category in ipairs(categories) do
|
||||
for _, v in ipairs(category) do
|
||||
table.insert(data, v)
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local function arrayEqual(a1, a2)
|
||||
if #a1 ~= #a2 then
|
||||
return false
|
||||
end
|
||||
|
||||
for i, v in ipairs(a1) do
|
||||
if v ~= a2[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function receive()
|
||||
l, e = gbSocket:receive()
|
||||
if e == 'closed' then
|
||||
if curstate == STATE_OK then
|
||||
print("Connection closed")
|
||||
end
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
elseif e == 'timeout' then
|
||||
return
|
||||
elseif e ~= nil then
|
||||
print(e)
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
if l ~= nil then
|
||||
block = json.decode(l)
|
||||
if block ~= nil then
|
||||
local itemsBlock = block["items"]
|
||||
if itemsBlock ~= nil then
|
||||
ItemsReceived = itemsBlock
|
||||
end
|
||||
deathlink_rec = block["deathlink"]
|
||||
|
||||
end
|
||||
end
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
newPlayerName = uRange(0xFFF0, 0x10)
|
||||
newSeedName = uRange(0xFFDB, 21)
|
||||
if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then
|
||||
print("ROM changed, quitting")
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
playerName = newPlayerName
|
||||
seedName = newSeedName
|
||||
local retTable = {}
|
||||
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||
|
||||
if compat == nil then
|
||||
compat = u8(ClientCompatibilityAddress)
|
||||
if compat < 2 then
|
||||
InGameAddress = 0x1A71
|
||||
end
|
||||
end
|
||||
|
||||
retTable["clientCompatibilityVersion"] = compat
|
||||
retTable["playerName"] = playerName
|
||||
retTable["seedName"] = seedName
|
||||
memDomain.wram()
|
||||
|
||||
in_game = u8(InGameAddress)
|
||||
if in_game == 0x2A or in_game == 0xAC then
|
||||
retTable["locations"] = generateLocationsChecked()
|
||||
elseif in_game ~= 0 then
|
||||
print("Game may have crashed")
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
|
||||
retTable["deathLink"] = deathlink_send
|
||||
deathlink_send = false
|
||||
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = gbSocket:send(msg)
|
||||
if ret == nil then
|
||||
print(error)
|
||||
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
|
||||
curstate = STATE_TENTATIVELY_CONNECTED
|
||||
elseif curstate == STATE_TENTATIVELY_CONNECTED then
|
||||
print("Connected!")
|
||||
curstate = STATE_OK
|
||||
end
|
||||
end
|
||||
|
||||
function main()
|
||||
if not checkBizHawkVersion() then
|
||||
return
|
||||
end
|
||||
server, error = socket.bind('localhost', 17242)
|
||||
|
||||
while true do
|
||||
frame = frame + 1
|
||||
if not (curstate == prevstate) then
|
||||
print("Current state: "..curstate)
|
||||
prevstate = curstate
|
||||
end
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 5 == 0) then
|
||||
receive()
|
||||
in_game = u8(InGameAddress)
|
||||
if in_game == 0x2A or in_game == 0xAC then
|
||||
if u8(APItemAddress) == 0x00 then
|
||||
ItemIndex = u16(APIndex)
|
||||
if deathlink_rec == true then
|
||||
wU8(APDeathLinkAddress, 1)
|
||||
elseif u8(APDeathLinkAddress) == 3 then
|
||||
wU8(APDeathLinkAddress, 0)
|
||||
deathlink_send = true
|
||||
end
|
||||
if ItemsReceived[ItemIndex + 1] ~= nil then
|
||||
item_id = ItemsReceived[ItemIndex + 1] - 172000000
|
||||
if item_id > 255 then
|
||||
item_id = item_id - 256
|
||||
end
|
||||
wU8(APItemAddress, item_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
if (frame % 60 == 0) then
|
||||
|
||||
print("Waiting for client.")
|
||||
|
||||
emu.frameadvance()
|
||||
server:settimeout(2)
|
||||
print("Attempting to connect")
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||
gbSocket = client
|
||||
gbSocket:settimeout(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
@@ -46,21 +46,12 @@
|
||||
# DOOM 1993
|
||||
/worlds/doom_1993/ @Daivuk
|
||||
|
||||
# DOOM II
|
||||
/worlds/doom_ii/ @Daivuk
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
# Final Fantasy
|
||||
/worlds/ff1/ @jtoyoda
|
||||
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
# Heretic
|
||||
/worlds/heretic/ @Daivuk
|
||||
|
||||
# Hollow Knight
|
||||
/worlds/hk/ @BadMagic100 @ThePhar
|
||||
|
||||
@@ -70,12 +61,6 @@
|
||||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
# Landstalker: The Treasures of King Nole
|
||||
/worlds/landstalker/ @Dinopony
|
||||
|
||||
# Lingo
|
||||
/worlds/lingo/ @hatkirby
|
||||
|
||||
# Links Awakening DX
|
||||
/worlds/ladx/ @zig-for
|
||||
|
||||
@@ -107,9 +92,6 @@
|
||||
# Overcooked! 2
|
||||
/worlds/overcooked2/ @toasterparty
|
||||
|
||||
# Pokemon Emerald
|
||||
/worlds/pokemon_emerald/ @Zunawe
|
||||
|
||||
# Pokemon Red and Blue
|
||||
/worlds/pokemon_rb/ @Alchav
|
||||
|
||||
@@ -122,9 +104,6 @@
|
||||
# Risk of Rain 2
|
||||
/worlds/ror2/ @kindasneaki
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ The zip can contain arbitrary files in addition what was specified above.
|
||||
|
||||
## Caveats
|
||||
|
||||
Imports from other files inside the apworld have to use relative imports. e.g. `from .options import MyGameOptions`
|
||||
Imports from other files inside the apworld have to use relative imports.
|
||||
|
||||
Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or
|
||||
`from worlds.AutoWorld import World`
|
||||
Imports from AP base have to use absolute imports, e.g. Options.py and worlds/AutoWorld.py.
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
# Contributing
|
||||
Contributions are welcome. We have a few requests for new contributors:
|
||||
Contributions are welcome. We have a few requests of any new contributors.
|
||||
|
||||
* **Follow styling guidelines.**
|
||||
Please take a look at the [code style documentation](/docs/style.md)
|
||||
to ensure ease of communication and uniformity.
|
||||
|
||||
* **Ensure that critical changes are covered by tests.**
|
||||
It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
|
||||
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/world%20api.md#tests).
|
||||
If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
|
||||
|
||||
* **Do not introduce unit test failures/regressions.**
|
||||
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
||||
your changes. Currently, the oldest supported version is [Python 3.8](https://www.python.org/downloads/release/python-380/).
|
||||
It is recommended that automated github actions are turned on in your fork to have github run all of the unit tests after pushing.
|
||||
You can turn them on here:
|
||||
* Follow styling as designated in our [styling documentation](/docs/style.md).
|
||||
* Ensure that all changes which affect logic are covered by unit tests.
|
||||
* Do not introduce any unit test failures/regressions.
|
||||
* Turn on automated github actions in your fork to have github run all the unit tests after pushing. See example below:
|
||||

|
||||
|
||||
Other than these requests, we tend to judge code on a case by case basis.
|
||||
Otherwise, we tend to judge code on a case to case basis.
|
||||
|
||||
For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md).
|
||||
|
||||
If you want to contribute to the core, you will be subject to stricter review on your pull requests. It is recommended
|
||||
that you get in touch with other core maintainers via the [Discord](https://archipelago.gg/discord).
|
||||
|
||||
If you want to add Archipelago support for a new game, please take a look at the [adding games documentation](/docs/adding%20games.md), which details what is required
|
||||
to implement support for a game, as well as tips for how to get started.
|
||||
If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a
|
||||
[world maintainer](/docs/world%20maintainer.md).
|
||||
|
||||
For other questions, feel free to explore the [main documentation folder](/docs/) and ask us questions in the #archipelago-dev channel
|
||||
of the [Discord](https://archipelago.gg/discord).
|
||||
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
|
||||
[the docs folder](/docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
|
||||
channel in our [Discord](https://archipelago.gg/discord).
|
||||
If you want to merge a new game, please make sure to read the responsibilities as
|
||||
[world maintainer](/docs/world%20maintainer.md).
|
||||
|
||||
@@ -380,12 +380,11 @@ Additional arguments sent in this package will also be added to the [Retrieved](
|
||||
|
||||
Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`.
|
||||
|
||||
| Name | Type | Notes |
|
||||
|------------------------------|-------------------------------|---------------------------------------------------|
|
||||
| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
|
||||
| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
|
||||
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
||||
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
|
||||
| Name | Type | Notes |
|
||||
|-------------------------------|--------------------------|---------------------------------------------------|
|
||||
| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
|
||||
| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
|
||||
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
||||
|
||||
### Set
|
||||
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
||||
@@ -416,8 +415,6 @@ The following operations can be applied to a datastorage key
|
||||
| mul | Multiplies the current value of the key by `value`. |
|
||||
| pow | Multiplies the current value of the key to the power of `value`. |
|
||||
| mod | Sets the current value of the key to the remainder after division by `value`. |
|
||||
| floor | Floors the current value (`value` is ignored). |
|
||||
| ceil | Ceils the current value (`value` is ignored). |
|
||||
| max | Sets the current value of the key to `value` if `value` is bigger. |
|
||||
| min | Sets the current value of the key to `value` if `value` is lower. |
|
||||
| and | Applies a bitwise AND to the current value of the key with `value`. |
|
||||
@@ -559,7 +556,7 @@ Color options:
|
||||
`player` marks owning player id for location/item,
|
||||
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
|
||||
|
||||
### ClientStatus
|
||||
### Client States
|
||||
An enumeration containing the possible client states that may be used to inform
|
||||
the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets
|
||||
the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection
|
||||
|
||||
@@ -31,7 +31,7 @@ As an example, suppose we want an option that lets the user start their game wit
|
||||
create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
|
||||
|
||||
```python
|
||||
# options.py
|
||||
# Options.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Toggle, PerGameCommonOptions
|
||||
@@ -77,33 +77,7 @@ or if I need a boolean object, such as in my slot_data I can access it as:
|
||||
```python
|
||||
start_with_sword = bool(self.options.starting_sword.value)
|
||||
```
|
||||
All numeric options (i.e. Toggle, Choice, Range) can be compared to integers, strings that match their attributes,
|
||||
strings that match the option attributes after "option_" is stripped, and the attributes themselves.
|
||||
```python
|
||||
# options.py
|
||||
class Logic(Choice):
|
||||
option_normal = 0
|
||||
option_hard = 1
|
||||
option_challenging = 2
|
||||
option_extreme = 3
|
||||
option_insane = 4
|
||||
alias_extra_hard = 2
|
||||
crazy = 4 # won't be listed as an option and only exists as an attribute on the class
|
||||
|
||||
# __init__.py
|
||||
from .options import Logic
|
||||
|
||||
if self.options.logic:
|
||||
do_things_for_all_non_normal_logic()
|
||||
if self.options.logic == 1:
|
||||
do_hard_things()
|
||||
elif self.options.logic == "challenging":
|
||||
do_challenging_things()
|
||||
elif self.options.logic == Logic.option_extreme:
|
||||
do_extreme_things()
|
||||
elif self.options.logic == "crazy":
|
||||
do_insane_things()
|
||||
```
|
||||
## Generic Option Classes
|
||||
These options are generically available to every game automatically, but can be overridden for slightly different
|
||||
behavior, if desired. See `worlds/soe/Options.py` for an example.
|
||||
@@ -170,20 +144,13 @@ A numeric option allowing a variety of integers including the endpoints. Has a d
|
||||
`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
|
||||
comparisons.
|
||||
|
||||
### NamedRange
|
||||
### SpecialRange
|
||||
Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value.
|
||||
`special_range_names` can be used to
|
||||
- give descriptive names to certain values from within the range
|
||||
- add option values above or below the regular range, to be associated with a special meaning
|
||||
|
||||
For example:
|
||||
```python
|
||||
range_start = 1
|
||||
range_end = 99
|
||||
special_range_names: {
|
||||
"normal": 20,
|
||||
"extreme": 99,
|
||||
"unlimited": -1,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -73,53 +73,6 @@ for your world specifically on the webhost:
|
||||
`game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be
|
||||
prefixed with the same string as defined here. Default already has 'en'.
|
||||
|
||||
`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values
|
||||
are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of
|
||||
the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page.
|
||||
|
||||
Note: The values must be a non-aliased value for the option type and can only include the following option types:
|
||||
|
||||
- If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end`
|
||||
values.
|
||||
- If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the
|
||||
`special_range_names` keys.
|
||||
- If you have a `Choice` option, the value should be a `str` that is one of the `option_<name>` values.
|
||||
- If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.
|
||||
- `random` is also a valid value for any of these option types.
|
||||
|
||||
`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time.
|
||||
|
||||
Here is an example of a defined preset:
|
||||
```python
|
||||
# presets.py
|
||||
options_presets = {
|
||||
"Limited Potential": {
|
||||
"progression_balancing": 0,
|
||||
"fairy_chests_per_zone": 2,
|
||||
"starting_class": "random",
|
||||
"chests_per_zone": 30,
|
||||
"vendors": "normal",
|
||||
"architect": "disabled",
|
||||
"gold_gain_multiplier": "half",
|
||||
"number_of_children": 2,
|
||||
"free_diary_on_generation": False,
|
||||
"health_pool": 10,
|
||||
"mana_pool": 10,
|
||||
"attack_pool": 10,
|
||||
"magic_damage_pool": 10,
|
||||
"armor_pool": 5,
|
||||
"equip_pool": 10,
|
||||
"crit_chance_pool": 5,
|
||||
"crit_damage_pool": 5,
|
||||
}
|
||||
}
|
||||
|
||||
# __init__.py
|
||||
class RLWeb(WebWorld):
|
||||
options_presets = options_presets
|
||||
# ...
|
||||
```
|
||||
|
||||
### MultiWorld Object
|
||||
|
||||
The `MultiWorld` object references the whole multiworld (all items and locations
|
||||
@@ -168,38 +121,6 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED
|
||||
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||
required, and will prevent progression and useful items from being placed at excluded locations.
|
||||
|
||||
#### Documenting Locations
|
||||
|
||||
Worlds can optionally provide a `location_descriptions` map which contains
|
||||
human-friendly descriptions of locations or location groups. These descriptions
|
||||
will show up in location-selection options in the Weighted Options page. Extra
|
||||
indentation and single newlines will be collapsed into spaces.
|
||||
|
||||
```python
|
||||
# Locations.py
|
||||
|
||||
location_descriptions = {
|
||||
"Red Potion #6": "In a secret destructible block under the second stairway",
|
||||
"L2 Spaceship": """
|
||||
The group of all items in the spaceship in Level 2.
|
||||
|
||||
This doesn't include the item on the spaceship door, since it can be
|
||||
accessed without the Spaeship Key.
|
||||
"""
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from .Locations import location_descriptions
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
location_descriptions = location_descriptions
|
||||
```
|
||||
|
||||
### Items
|
||||
|
||||
Items are all things that can "drop" for your game. This may be RPG items like
|
||||
@@ -226,37 +147,6 @@ Other classifications include
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens
|
||||
|
||||
#### Documenting Items
|
||||
|
||||
Worlds can optionally provide an `item_descriptions` map which contains
|
||||
human-friendly descriptions of items or item groups. These descriptions will
|
||||
show up in item-selection options in the Weighted Options page. Extra
|
||||
indentation and single newlines will be collapsed into spaces.
|
||||
|
||||
```python
|
||||
# Items.py
|
||||
|
||||
item_descriptions = {
|
||||
"Red Potion": "A standard health potion",
|
||||
"Spaceship Key": """
|
||||
The key to the spaceship in Level 2.
|
||||
|
||||
This is necessary to get to the Star Realm.
|
||||
"""
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from .Items import item_descriptions
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
item_descriptions = item_descriptions
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
Events will mark some progress. You define an event location, an
|
||||
@@ -333,11 +223,11 @@ See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requireme
|
||||
AP will only import the `__init__.py`. Depending on code size it makes sense to
|
||||
use multiple files and use relative imports to access them.
|
||||
|
||||
e.g. `from .options import MyGameOptions` from your `__init__.py` will load
|
||||
`world/[world_name]/options.py` and make its `MyGameOptions` accessible.
|
||||
e.g. `from .Options import MyGameOptions` from your `__init__.py` will load
|
||||
`world/[world_name]/Options.py` and make its `MyGameOptions` accessible.
|
||||
|
||||
When imported names pile up it may be easier to use `from . import options`
|
||||
and access the variable as `options.MyGameOptions`.
|
||||
When imported names pile up it may be easier to use `from . import Options`
|
||||
and access the variable as `Options.MyGameOptions`.
|
||||
|
||||
Imports from directories outside your world should use absolute imports.
|
||||
Correct use of relative / absolute imports is required for zipped worlds to
|
||||
@@ -358,7 +248,7 @@ class MyGameItem(Item):
|
||||
game: str = "My Game"
|
||||
```
|
||||
By convention this class definition will either be placed in your `__init__.py`
|
||||
or your `items.py`. For a more elaborate example see `worlds/oot/Items.py`.
|
||||
or your `Items.py`. For a more elaborate example see `worlds/oot/Items.py`.
|
||||
|
||||
### Your location type
|
||||
|
||||
@@ -370,15 +260,15 @@ class MyGameLocation(Location):
|
||||
game: str = "My Game"
|
||||
|
||||
# override constructor to automatically mark event locations as such
|
||||
def __init__(self, player: int, name = "", code = None, parent = None) -> None:
|
||||
def __init__(self, player: int, name = "", code = None, parent = None):
|
||||
super(MyGameLocation, self).__init__(player, name, code, parent)
|
||||
self.event = code is None
|
||||
```
|
||||
in your `__init__.py` or your `locations.py`.
|
||||
in your `__init__.py` or your `Locations.py`.
|
||||
|
||||
### Options
|
||||
|
||||
By convention options are defined in `options.py` and will be used when parsing
|
||||
By convention options are defined in `Options.py` and will be used when parsing
|
||||
the players' yaml files.
|
||||
|
||||
Each option has its own class, inherits from a base option type, has a docstring
|
||||
@@ -394,7 +284,7 @@ For more see `Options.py` in AP's base directory.
|
||||
|
||||
#### Toggle, DefaultOnToggle
|
||||
|
||||
These don't need any additional properties defined. After parsing the option,
|
||||
Those don't need any additional properties defined. After parsing the option,
|
||||
its `value` will either be True or False.
|
||||
|
||||
#### Range
|
||||
@@ -420,7 +310,7 @@ default = 0
|
||||
|
||||
#### Sample
|
||||
```python
|
||||
# options.py
|
||||
# Options.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
from Options import Toggle, Range, Choice, PerGameCommonOptions
|
||||
@@ -459,7 +349,7 @@ class MyGameOptions(PerGameCommonOptions):
|
||||
# __init__.py
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from .options import MyGameOptions # import the options dataclass
|
||||
from .Options import MyGameOptions # import the options dataclass
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
@@ -476,9 +366,9 @@ class MyGameWorld(World):
|
||||
|
||||
import settings
|
||||
import typing
|
||||
from .options import MyGameOptions # the options we defined earlier
|
||||
from .items import mygame_items # data used below to add items to the World
|
||||
from .locations import mygame_locations # same as above
|
||||
from .Options import MyGameOptions # the options we defined earlier
|
||||
from .Items import mygame_items # data used below to add items to the World
|
||||
from .Locations import mygame_locations # same as above
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
|
||||
|
||||
@@ -537,7 +427,7 @@ The world has to provide the following things for generation
|
||||
* additions to the regions list: at least one called "Menu"
|
||||
* locations placed inside those regions
|
||||
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||
* applying `self.multiworld.push_precollected` for world defined start inventory
|
||||
* applying `self.multiworld.push_precollected` for start inventory
|
||||
* `required_client_version: Tuple[int, int, int]`
|
||||
Optional client version as tuple of 3 ints to make sure the client is compatible to
|
||||
this world (e.g. implements all required features) when connecting.
|
||||
@@ -547,32 +437,31 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
* `stage_assert_generate(cls, multiworld)` is a class method called at the start of
|
||||
generation to check the existence of prerequisite files, usually a ROM for
|
||||
games which require one.
|
||||
* `generate_early(self)`
|
||||
called per player before any items or locations are created. You can set properties on your world here. Already has
|
||||
access to player options and RNG. This is the earliest step where the world should start setting up for the current
|
||||
multiworld as any steps before this, the multiworld itself is still getting set up
|
||||
* `create_regions(self)`
|
||||
* `def generate_early(self)`
|
||||
called per player before any items or locations are created. You can set
|
||||
properties on your world here. Already has access to player options and RNG.
|
||||
* `def create_regions(self)`
|
||||
called to place player's regions and their locations into the MultiWorld's regions list. If it's
|
||||
hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||
* `create_items(self)`
|
||||
* `def create_items(self)`
|
||||
called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in
|
||||
the MultiWorld's regions and itempool, and these lists should not be modified afterwards.
|
||||
* `set_rules(self)`
|
||||
* `def set_rules(self)`
|
||||
called to set access and item rules on locations and entrances.
|
||||
Locations have to be defined before this, or rule application can miss them.
|
||||
* `generate_basic(self)`
|
||||
* `def generate_basic(self)`
|
||||
called after the previous steps. Some placement and player specific
|
||||
randomizations can be done here.
|
||||
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` are called to modify item placement
|
||||
* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
|
||||
before, during and after the regular fill process, before `generate_output`.
|
||||
If items need to be placed during pre_fill, these items can be determined
|
||||
and created using `get_prefill_items`
|
||||
* `generate_output(self, output_directory: str)` that creates the output
|
||||
* `def generate_output(self, output_directory: str)` that creates the output
|
||||
files if there is output to be generated. When this is
|
||||
called, `self.multiworld.get_locations(self.player)` has all locations for the player, with
|
||||
attribute `item` pointing to the item.
|
||||
`location.item.player` can be used to see if it's a local item.
|
||||
* `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that
|
||||
* `fill_slot_data` and `modify_multidata` can be used to modify the data that
|
||||
will be used by the server to host the MultiWorld.
|
||||
|
||||
|
||||
@@ -589,9 +478,9 @@ def generate_early(self) -> None:
|
||||
```python
|
||||
# we need a way to know if an item provides progress in the game ("key item")
|
||||
# this can be part of the items definition, or depend on recipe randomization
|
||||
from .items import is_progression # this is just a dummy
|
||||
from .Items import is_progression # this is just a dummy
|
||||
|
||||
def create_item(self, item: str) -> MyGameItem:
|
||||
def create_item(self, item: str):
|
||||
# This is called when AP wants to create an item by name (for plando) or
|
||||
# when you call it from your own code.
|
||||
classification = ItemClassification.progression if is_progression(item) else \
|
||||
@@ -599,7 +488,7 @@ def create_item(self, item: str) -> MyGameItem:
|
||||
return MyGameItem(item, classification, self.item_name_to_id[item],
|
||||
self.player)
|
||||
|
||||
def create_event(self, event: str) -> MyGameItem:
|
||||
def create_event(self, event: str):
|
||||
# while we are at it, we can also add a helper to create events
|
||||
return MyGameItem(event, True, None, self.player)
|
||||
```
|
||||
@@ -691,8 +580,8 @@ def generate_basic(self) -> None:
|
||||
### Setting Rules
|
||||
|
||||
```python
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item, add_item_rule
|
||||
from .items import get_item_type
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
from Items import get_item_type
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
@@ -718,7 +607,7 @@ def set_rules(self) -> None:
|
||||
# require one item from an item group
|
||||
add_rule(self.multiworld.get_location("Chest3", self.player),
|
||||
lambda state: state.has_group("weapons", self.player))
|
||||
# state also has .count() for items, .has_any() and .has_all() for multiple
|
||||
# state also has .item_count() for items, .has_any() and .has_all() for sets
|
||||
# and .count_group() for groups
|
||||
# set_rule is likely to be a bit faster than add_rule
|
||||
|
||||
@@ -761,12 +650,12 @@ Please do this with caution and only when necessary.
|
||||
#### Sample
|
||||
|
||||
```python
|
||||
# logic.py
|
||||
# Logic.py
|
||||
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
class MyGameLogic(LogicMixin):
|
||||
def mygame_has_key(self, player: int) -> bool:
|
||||
def mygame_has_key(self, player: int):
|
||||
# Arguments above are free to choose
|
||||
# MultiWorld can be accessed through self.multiworld, explicitly passing in
|
||||
# MyGameWorld instance for easy options access is also a valid approach
|
||||
@@ -776,11 +665,11 @@ class MyGameLogic(LogicMixin):
|
||||
# __init__.py
|
||||
|
||||
from worlds.generic.Rules import set_rule
|
||||
import .logic # apply the mixin by importing its file
|
||||
import .Logic # apply the mixin by importing its file
|
||||
|
||||
class MyGameWorld(World):
|
||||
# ...
|
||||
def set_rules(self) -> None:
|
||||
def set_rules(self):
|
||||
set_rule(self.multiworld.get_location("A Door", self.player),
|
||||
lambda state: state.mygame_has_key(self.player))
|
||||
```
|
||||
@@ -788,10 +677,10 @@ class MyGameWorld(World):
|
||||
### Generate Output
|
||||
|
||||
```python
|
||||
from .mod import generate_mod
|
||||
from .Mod import generate_mod
|
||||
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
def generate_output(self, output_directory: str):
|
||||
# How to generate the mod or ROM highly depends on the game
|
||||
# if the mod is written in Lua, Jinja can be used to fill a template
|
||||
# if the mod reads a json file, `json.dump()` can be used to generate that
|
||||
@@ -806,10 +695,12 @@ def generate_output(self, output_directory: str) -> None:
|
||||
# make sure to mark as not remote_start_inventory when connecting if stored in rom/mod
|
||||
"starter_items": [item.name for item
|
||||
in self.multiworld.precollected_items[self.player]],
|
||||
"final_boss_hp": self.final_boss_hp,
|
||||
# store option name "easy", "normal" or "hard" for difficuly
|
||||
"difficulty": self.options.difficulty.current_key,
|
||||
# store option value True or False for fixing a glitch
|
||||
"fix_xyz_glitch": self.options.fix_xyz_glitch.value,
|
||||
}
|
||||
|
||||
# add needed option results to the dictionary
|
||||
data.update(self.options.as_dict("final_boss_hp", "difficulty", "fix_xyz_glitch"))
|
||||
# point to a ROM specified by the installation
|
||||
src = self.settings.rom_file
|
||||
# or point to worlds/mygame/data/mod_template
|
||||
@@ -833,7 +724,7 @@ data already exists on the server. The most common usage of slot data is to send
|
||||
to be aware of.
|
||||
|
||||
```python
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
def fill_slot_data(self):
|
||||
# in order for our game client to handle the generated seed correctly we need to know what the user selected
|
||||
# for their difficulty and final boss HP
|
||||
# a dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting
|
||||
@@ -885,14 +776,14 @@ from . import MyGameTestBase
|
||||
|
||||
|
||||
class TestChestAccess(MyGameTestBase):
|
||||
def test_sword_chests(self) -> None:
|
||||
def test_sword_chests(self):
|
||||
"""Test locations that require a sword"""
|
||||
locations = ["Chest1", "Chest2"]
|
||||
items = [["Sword"]]
|
||||
# this will test that each location can't be accessed without the "Sword", but can be accessed once obtained.
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def test_any_weapon_chests(self) -> None:
|
||||
def test_any_weapon_chests(self):
|
||||
"""Test locations that require any weapon"""
|
||||
locations = [f"Chest{i}" for i in range(3, 6)]
|
||||
items = [["Sword"], ["Axe"], ["Spear"]]
|
||||
|
||||
@@ -80,10 +80,7 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[InstallDelete]
|
||||
Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld"
|
||||
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
||||
Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
|
||||
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
||||
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
|
||||
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||
@@ -143,24 +140,19 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{
|
||||
|
||||
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apemerald"; ValueData: "{#MyAppName}pkmnepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Archipelago Pokemon Emerald Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
263
kvui.py
263
kvui.py
@@ -5,13 +5,12 @@ import typing
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
|
||||
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
|
||||
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||
except FileNotFoundError: # shcore may not be found on <= Windows 7
|
||||
pass # TODO: remove silent except when Python 3.8 is phased out.
|
||||
pass # TODO: remove silent except when Python 3.8 is phased out.
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
@@ -19,15 +18,14 @@ os.environ["KIVY_NO_ARGS"] = "1"
|
||||
os.environ["KIVY_LOG_ENABLE"] = "0"
|
||||
|
||||
import Utils
|
||||
|
||||
if Utils.is_frozen():
|
||||
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
|
||||
|
||||
from kivy.config import Config
|
||||
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set("kivy", "exit_on_escape", "0")
|
||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||
Config.set('kivy', 'exit_on_escape', '0')
|
||||
Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.core.window import Window
|
||||
@@ -60,6 +58,7 @@ from kivy.uix.popup import Popup
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
|
||||
from Utils import async_start
|
||||
|
||||
@@ -78,8 +77,8 @@ class HoverBehavior(object):
|
||||
border_point = ObjectProperty(None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.register_event_type("on_enter")
|
||||
self.register_event_type("on_leave")
|
||||
self.register_event_type('on_enter')
|
||||
self.register_event_type('on_leave')
|
||||
Window.bind(mouse_pos=self.on_mouse_pos)
|
||||
Window.bind(on_cursor_leave=self.on_cursor_leave)
|
||||
super(HoverBehavior, self).__init__(**kwargs)
|
||||
@@ -107,7 +106,7 @@ class HoverBehavior(object):
|
||||
self.dispatch("on_leave")
|
||||
|
||||
|
||||
Factory.register("HoverBehavior", HoverBehavior)
|
||||
Factory.register('HoverBehavior', HoverBehavior)
|
||||
|
||||
|
||||
class ToolTip(Label):
|
||||
@@ -122,60 +121,6 @@ class HovererableLabel(HoverBehavior, Label):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipLabel(HovererableLabel):
|
||||
tooltip = None
|
||||
|
||||
def create_tooltip(self, text, x, y):
|
||||
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
||||
if self.tooltip:
|
||||
# update
|
||||
self.tooltip.children[0].text = text
|
||||
else:
|
||||
self.tooltip = FloatLayout()
|
||||
tooltip_label = ToolTip(text=text)
|
||||
self.tooltip.add_widget(tooltip_label)
|
||||
fade_in_animation.start(self.tooltip)
|
||||
App.get_running_app().root.add_widget(self.tooltip)
|
||||
|
||||
# handle left-side boundary to not render off-screen
|
||||
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
|
||||
|
||||
# position float layout
|
||||
self.tooltip.x = x - self.tooltip.width / 2
|
||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
||||
|
||||
def remove_tooltip(self):
|
||||
if self.tooltip:
|
||||
App.get_running_app().root.remove_widget(self.tooltip)
|
||||
self.tooltip = None
|
||||
|
||||
def on_mouse_pos(self, window, pos):
|
||||
if not self.get_root_window():
|
||||
return # Abort if not displayed
|
||||
super().on_mouse_pos(window, pos)
|
||||
if self.refs and self.hovered:
|
||||
|
||||
tx, ty = self.to_widget(*pos, relative=True)
|
||||
# Why TF is Y flipped *within* the texture?
|
||||
ty = self.texture_size[1] - ty
|
||||
hit = False
|
||||
for uid, zones in self.refs.items():
|
||||
for zone in zones:
|
||||
x, y, w, h = zone
|
||||
if x <= tx <= w and y <= ty <= h:
|
||||
self.create_tooltip(uid.split("|", 1)[1], *pos)
|
||||
hit = True
|
||||
break
|
||||
if not hit:
|
||||
self.remove_tooltip()
|
||||
|
||||
def on_enter(self):
|
||||
pass
|
||||
|
||||
def on_leave(self):
|
||||
self.remove_tooltip()
|
||||
|
||||
|
||||
class ServerLabel(HovererableLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||
@@ -244,10 +189,11 @@ class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
|
||||
""" Adds selection and focus behaviour to the view. """
|
||||
|
||||
|
||||
class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
class SelectableLabel(RecycleDataViewBehavior, HovererableLabel):
|
||||
""" Add selection support to the Label """
|
||||
index = None
|
||||
selected = BooleanProperty(False)
|
||||
tooltip = None
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
""" Catch and handle the view changes """
|
||||
@@ -255,6 +201,56 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
return super(SelectableLabel, self).refresh_view_attrs(
|
||||
rv, index, data)
|
||||
|
||||
def create_tooltip(self, text, x, y):
|
||||
text = text.replace("<br>", "\n").replace('&', '&').replace('&bl;', '[').replace('&br;', ']')
|
||||
if self.tooltip:
|
||||
# update
|
||||
self.tooltip.children[0].text = text
|
||||
else:
|
||||
self.tooltip = FloatLayout()
|
||||
tooltip_label = ToolTip(text=text)
|
||||
self.tooltip.add_widget(tooltip_label)
|
||||
fade_in_animation.start(self.tooltip)
|
||||
App.get_running_app().root.add_widget(self.tooltip)
|
||||
|
||||
# handle left-side boundary to not render off-screen
|
||||
x = max(x, 3+self.tooltip.children[0].texture_size[0] / 2)
|
||||
|
||||
# position float layout
|
||||
self.tooltip.x = x - self.tooltip.width / 2
|
||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
||||
|
||||
def remove_tooltip(self):
|
||||
if self.tooltip:
|
||||
App.get_running_app().root.remove_widget(self.tooltip)
|
||||
self.tooltip = None
|
||||
|
||||
def on_mouse_pos(self, window, pos):
|
||||
if not self.get_root_window():
|
||||
return # Abort if not displayed
|
||||
super().on_mouse_pos(window, pos)
|
||||
if self.refs and self.hovered:
|
||||
|
||||
tx, ty = self.to_widget(*pos, relative=True)
|
||||
# Why TF is Y flipped *within* the texture?
|
||||
ty = self.texture_size[1] - ty
|
||||
hit = False
|
||||
for uid, zones in self.refs.items():
|
||||
for zone in zones:
|
||||
x, y, w, h = zone
|
||||
if x <= tx <= w and y <= ty <= h:
|
||||
self.create_tooltip(uid.split("|", 1)[1], *pos)
|
||||
hit = True
|
||||
break
|
||||
if not hit:
|
||||
self.remove_tooltip()
|
||||
|
||||
def on_enter(self):
|
||||
pass
|
||||
|
||||
def on_leave(self):
|
||||
self.remove_tooltip()
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
""" Add selection on touch down """
|
||||
if super(SelectableLabel, self).on_touch_down(touch):
|
||||
@@ -278,7 +274,7 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
elif not cmdinput.text and text.startswith("Missing: "):
|
||||
cmdinput.text = text.replace("Missing: ", "!hint_location ")
|
||||
|
||||
Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
Clipboard.copy(text.replace('&', '&').replace('&bl;', '[').replace('&br;', ']'))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
@@ -286,68 +282,9 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
index = None
|
||||
no_select = []
|
||||
|
||||
def __init__(self):
|
||||
super(HintLabel, self).__init__()
|
||||
self.receiving_text = ""
|
||||
self.item_text = ""
|
||||
self.finding_text = ""
|
||||
self.location_text = ""
|
||||
self.entrance_text = ""
|
||||
self.found_text = ""
|
||||
for child in self.children:
|
||||
child.bind(texture_size=self.set_height)
|
||||
|
||||
def set_height(self, instance, value):
|
||||
self.height = max([child.texture_size[1] for child in self.children])
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
self.index = index
|
||||
if "select" in data and not data["select"] and index not in self.no_select:
|
||||
self.no_select.append(index)
|
||||
self.striped = data["striped"]
|
||||
self.receiving_text = data["receiving"]["text"]
|
||||
self.item_text = data["item"]["text"]
|
||||
self.finding_text = data["finding"]["text"]
|
||||
self.location_text = data["location"]["text"]
|
||||
self.entrance_text = data["entrance"]["text"]
|
||||
self.found_text = data["found"]["text"]
|
||||
self.height = self.minimum_height
|
||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
""" Add selection on touch down """
|
||||
if super(HintLabel, self).on_touch_down(touch):
|
||||
return True
|
||||
if self.index not in self.no_select:
|
||||
if self.collide_point(*touch.pos):
|
||||
if self.selected:
|
||||
self.parent.clear_selection()
|
||||
else:
|
||||
text = "".join([self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
||||
self.finding_text, "\'s World", (" at " + self.entrance_text)
|
||||
if self.entrance_text != "Vanilla"
|
||||
else "", ". (", self.found_text.lower(), ")"])
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
""" Respond to the selection of items in the view. """
|
||||
if self.index not in self.no_select:
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class ConnectBarTextInput(TextInput):
|
||||
def insert_text(self, substring, from_undo=False):
|
||||
s = substring.replace("\n", "").replace("\r", "")
|
||||
s = substring.replace('\n', '').replace('\r', '')
|
||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
||||
|
||||
|
||||
@@ -365,7 +302,7 @@ class MessageBox(Popup):
|
||||
def __init__(self, title, text, error=False, **kwargs):
|
||||
label = MessageBox.MessageBoxLabel(text=text)
|
||||
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
|
||||
super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40),
|
||||
super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width)+40),
|
||||
separator_color=separator_color, **kwargs)
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
@@ -421,14 +358,11 @@ class GameManager(App):
|
||||
# top part
|
||||
server_label = ServerLabel()
|
||||
self.connect_layout.add_widget(server_label)
|
||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||
size_hint_y=None,
|
||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None,
|
||||
height=dp(30), multiline=False, write_tab=False)
|
||||
|
||||
def connect_bar_validate(sender):
|
||||
if not self.ctx.server:
|
||||
self.connect_button_action(sender)
|
||||
|
||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||
self.connect_layout.add_widget(self.server_connect_bar)
|
||||
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
|
||||
@@ -449,22 +383,20 @@ class GameManager(App):
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
panel = TabbedPanelItem(text=display_name)
|
||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
||||
if len(self.logging_pairs) > 1:
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = TabbedPanelItem(text="Hints")
|
||||
self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
|
||||
self.tabs.add_widget(hint_panel)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
self.tabs.default_tab_text = "Archipelago"
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
# Hide Tab selection if only one tab
|
||||
self.tabs.clear_tabs()
|
||||
self.tabs.do_default_tab = False
|
||||
self.tabs.current_tab.height = 0
|
||||
self.tabs.tab_height = 0
|
||||
|
||||
# bottom part
|
||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
||||
@@ -490,7 +422,7 @@ class GameManager(App):
|
||||
return self.container
|
||||
|
||||
def update_texts(self, dt):
|
||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
||||
if hasattr(self.tabs.content.children[0], 'fix_heights'):
|
||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
@@ -567,10 +499,6 @@ class GameManager(App):
|
||||
if hasattr(self, "energy_link_label"):
|
||||
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
||||
|
||||
def update_hints(self):
|
||||
hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
|
||||
self.log_panels["Hints"].refresh_hints(hints)
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
def open_settings(self, *largs):
|
||||
pass
|
||||
@@ -585,12 +513,12 @@ class LogtoUI(logging.Handler):
|
||||
def format_compact(record: logging.LogRecord) -> str:
|
||||
if isinstance(record.msg, Exception):
|
||||
return str(record.msg)
|
||||
return (f"{record.exc_info[1]}\n" if record.exc_info else "") + str(record.msg).split("\n")[0]
|
||||
return (f'{record.exc_info[1]}\n' if record.exc_info else '') + str(record.msg).split("\n")[0]
|
||||
|
||||
def handle(self, record: logging.LogRecord) -> None:
|
||||
if getattr(record, "skip_gui", False):
|
||||
if getattr(record, 'skip_gui', False):
|
||||
pass # skip output
|
||||
elif getattr(record, "compact_gui", False):
|
||||
elif getattr(record, 'compact_gui', False):
|
||||
self.on_log(self.format_compact(record))
|
||||
else:
|
||||
self.on_log(self.format(record))
|
||||
@@ -624,44 +552,6 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
"item": {"text": "[u]Item[/u]"},
|
||||
"finding": {"text": "[u]Finding Player[/u]"},
|
||||
"location": {"text": "[u]Location[/u]"},
|
||||
"entrance": {"text": "[u]Entrance[/u]"},
|
||||
"found": {"text": "[u]Status[/u]"},
|
||||
"striped": True,
|
||||
"select": False,
|
||||
}
|
||||
|
||||
def __init__(self, parser):
|
||||
super(HintLog, self).__init__()
|
||||
self.data = [self.header]
|
||||
self.parser = parser
|
||||
|
||||
def refresh_hints(self, hints):
|
||||
self.data = [self.header]
|
||||
striped = False
|
||||
for hint in hints:
|
||||
self.data.append({
|
||||
"striped": striped,
|
||||
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
||||
"item": {"text": self.parser.handle_node(
|
||||
{"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})},
|
||||
"finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})},
|
||||
"location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})},
|
||||
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
|
||||
"color": "blue", "text": hint["entrance"]
|
||||
if hint["entrance"] else "Vanilla"})},
|
||||
"found": {
|
||||
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
|
||||
"text": "Found" if hint["found"] else "Not Found"})},
|
||||
})
|
||||
striped = not striped
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
@@ -709,7 +599,7 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
f"Type: {SlotType(slot_info.type).name}"
|
||||
if slot_info.group_members:
|
||||
text += f"<br>Members:<br> " + \
|
||||
"<br> ".join(self.ctx.player_names[player] for player in slot_info.group_members)
|
||||
'<br> '.join(self.ctx.player_names[player] for player in slot_info.group_members)
|
||||
node.setdefault("refs", []).append(text)
|
||||
return super(KivyJSONtoTextParser, self)._handle_player_id(node)
|
||||
|
||||
@@ -737,3 +627,4 @@ user_file = Utils.user_path("data", "user.kv")
|
||||
if os.path.exists(user_file):
|
||||
logging.info("Loading user.kv into builder.")
|
||||
Builder.load_file(user_file)
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
colorama>=0.4.5
|
||||
websockets>=11.0.3
|
||||
PyYAML>=6.0.1
|
||||
jellyfish>=1.0.3
|
||||
jellyfish>=1.0.1
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.2.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.0.0
|
||||
certifi>=2023.11.17
|
||||
cython>=3.0.5
|
||||
bsdiff4>=1.2.3
|
||||
platformdirs>=3.9.1
|
||||
certifi>=2023.7.22
|
||||
cython>=0.29.35
|
||||
cymem>=2.0.8
|
||||
orjson>=3.9.10
|
||||
5
setup.py
5
setup.py
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze>=6.15.10'
|
||||
requirement = 'cx-Freeze>=6.15.2'
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
@@ -71,6 +71,7 @@ non_apworlds: set = {
|
||||
"Clique",
|
||||
"DLCQuest",
|
||||
"Final Fantasy",
|
||||
"Kingdom Hearts 2",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
@@ -619,7 +620,7 @@ cx_Freeze.setup(
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import random
|
||||
import sys
|
||||
import typing
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
|
||||
from Generate import get_seed_name
|
||||
from test.general import gen_steps
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
@@ -154,8 +152,6 @@ class WorldTestBase(unittest.TestCase):
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed(seed)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
random.seed(self.multiworld.seed)
|
||||
self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
|
||||
@@ -442,47 +442,6 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1")
|
||||
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule2(self):
|
||||
"""Test that swap works before all items are placed"""
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 5, 5)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
# Two items provide access to sphere 2.
|
||||
# One of them is forbidden in sphere 1, the other is first placed in sphere 4 because of placement order,
|
||||
# requiring a swap.
|
||||
# There are spheres in between, so for the swap to work, it'll have to assume all other items are collected.
|
||||
one_to_two1 = items[4].name
|
||||
one_to_two2 = items[3].name
|
||||
three_to_four = items[2].name
|
||||
two_to_three1 = items[1].name
|
||||
two_to_three2 = items[0].name
|
||||
# Sphere 4
|
||||
set_rule(locations[0], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id))
|
||||
and state.has(two_to_three1, player1.id)
|
||||
and state.has(two_to_three2, player1.id)
|
||||
and state.has(three_to_four, player1.id)))
|
||||
# Sphere 3
|
||||
set_rule(locations[1], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id))
|
||||
and state.has(two_to_three1, player1.id)
|
||||
and state.has(two_to_three2, player1.id)))
|
||||
# Sphere 2
|
||||
set_rule(locations[2], lambda state: state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id))
|
||||
# Sphere 1
|
||||
sphere1_loc1 = locations[3]
|
||||
sphere1_loc2 = locations[4]
|
||||
# forbid one_to_two2 in sphere 1 to make the swap happen as described above
|
||||
add_item_rule(sphere1_loc1, lambda item_to_place: item_to_place.name != one_to_two2)
|
||||
add_item_rule(sphere1_loc2, lambda item_to_place: item_to_place.name != one_to_two2)
|
||||
|
||||
# Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap,
|
||||
# which it will attempt before two_to_three and three_to_four are placed, testing the behavior.
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items)
|
||||
# assert swap happened
|
||||
self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1")
|
||||
self.assertTrue(sphere1_loc1.item.name == one_to_two1 or
|
||||
sphere1_loc2.item.name == one_to_two1, "Wrong item in Sphere 1")
|
||||
|
||||
def test_double_sweep(self):
|
||||
"""Test that sweep doesn't duplicate Event items when sweeping"""
|
||||
# test for PR1114
|
||||
|
||||
@@ -40,8 +40,8 @@ class TestImplemented(unittest.TestCase):
|
||||
# has an await for generate_output which isn't being called
|
||||
if game_name in {"Ocarina of Time", "Zillion"}:
|
||||
continue
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
with self.subTest(game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||
|
||||
@@ -60,12 +60,3 @@ class TestBase(unittest.TestCase):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
for item in multiworld.itempool:
|
||||
self.assertIn(item.name, world_type.item_name_to_id)
|
||||
|
||||
def test_item_descriptions_have_valid_names(self):
|
||||
"""Ensure all item descriptions match an item name or item group name"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
valid_names = world_type.item_names.union(world_type.item_name_groups)
|
||||
for name in world_type.item_descriptions:
|
||||
with self.subTest("Name should be valid", game=game_name, item=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All item descriptions must match defined item names")
|
||||
|
||||
@@ -66,12 +66,3 @@ class TestBase(unittest.TestCase):
|
||||
for location in locations:
|
||||
self.assertIn(location, world_type.location_name_to_id)
|
||||
self.assertNotIn(group_name, world_type.location_name_to_id)
|
||||
|
||||
def test_location_descriptions_have_valid_names(self):
|
||||
"""Ensure all location descriptions match a location name or location group name"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
valid_names = world_type.location_names.union(world_type.location_name_groups)
|
||||
for name in world_type.location_descriptions:
|
||||
with self.subTest("Name should be valid", game=game_name, location=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All location descriptions must match defined location names")
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import Choice, NamedRange, Toggle, Range
|
||||
|
||||
|
||||
class TestOptionPresets(unittest.TestCase):
|
||||
def test_option_presets_have_valid_options(self):
|
||||
"""Test that all predefined option presets are valid options."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
presets = world_type.web.options_presets
|
||||
for preset_name, preset in presets.items():
|
||||
for option_name, option_value in preset.items():
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
try:
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
supported_types = [Choice, Toggle, Range, NamedRange]
|
||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||
f"is not a supported type for webhost. "
|
||||
f"Supported types: {', '.join([t.__name__ for t in supported_types])}")
|
||||
except AssertionError as ex:
|
||||
self.fail(f"Option '{option_name}': '{option_value}' in preset '{preset_name}' for game "
|
||||
f"'{game_name}' is not valid. Error: {ex}")
|
||||
except KeyError as ex:
|
||||
self.fail(f"Option '{option_name}' in preset '{preset_name}' for game '{game_name}' is "
|
||||
f"not a defined option. Error: {ex}")
|
||||
|
||||
def test_option_preset_values_are_explicitly_defined(self):
|
||||
"""Test that option preset values are not a special flavor of 'random' or use from_text to resolve another
|
||||
value.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
presets = world_type.web.options_presets
|
||||
for preset_name, preset in presets.items():
|
||||
for option_name, option_value in preset.items():
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
# Check for non-standard random values.
|
||||
self.assertFalse(
|
||||
str(option_value).startswith("random-"),
|
||||
f"'{option_name}': '{option_value}' in preset '{preset_name}' for game '{game_name}' "
|
||||
f"is not supported for webhost. Special random values are not supported for presets."
|
||||
)
|
||||
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
|
||||
# Check for from_text resolving to a different value. ("random" is allowed though.)
|
||||
if option_value != "random" and isinstance(option_value, str):
|
||||
# Allow special named values for NamedRange option presets.
|
||||
if isinstance(option, NamedRange):
|
||||
self.assertTrue(
|
||||
option_value in option.special_range_names,
|
||||
f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' "
|
||||
f"for game '{game_name}'. Expected {option.special_range_names.keys()} or "
|
||||
f"{option.range_start}-{option.range_end}."
|
||||
)
|
||||
else:
|
||||
self.assertTrue(
|
||||
option.name_lookup.get(option.value, None) == option_value,
|
||||
f"'{option_name}': '{option_value}' in preset '{preset_name}' for game "
|
||||
f"'{game_name}' is not supported for webhost. Values must not be resolved to a "
|
||||
f"different option via option.from_text (or an alias)."
|
||||
)
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import make_dataclass
|
||||
@@ -52,17 +51,11 @@ class AutoWorldRegister(type):
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
dct["item_descriptions"] = {name: _normalize_description(description) for name, description
|
||||
in dct.get("item_descriptions", {}).items()}
|
||||
dct["item_descriptions"]["Everything"] = "All items in the entire game."
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
dct["location_descriptions"] = {name: _normalize_description(description) for name, description
|
||||
in dct.get("location_descriptions", {}).items()}
|
||||
dct["location_descriptions"]["Everywhere"] = "All locations in the entire game."
|
||||
|
||||
# move away from get_required_client_version function
|
||||
if "game" in dct:
|
||||
@@ -120,10 +113,10 @@ def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
taken = time.perf_counter() - start
|
||||
if taken > 1.0:
|
||||
if player and multiworld:
|
||||
perf_logger.info(f"Took {taken:.4f} seconds in {method.__qualname__} for player {player}, "
|
||||
perf_logger.info(f"Took {taken} seconds in {method.__qualname__} for player {player}, "
|
||||
f"named {multiworld.player_name[player]}.")
|
||||
else:
|
||||
perf_logger.info(f"Took {taken:.4f} seconds in {method.__qualname__}.")
|
||||
perf_logger.info(f"Took {taken} seconds in {method.__qualname__}.")
|
||||
return ret
|
||||
|
||||
|
||||
@@ -186,9 +179,6 @@ class WebWorld:
|
||||
bug_report_page: Optional[str]
|
||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||
|
||||
options_presets: Dict[str, Dict[str, Any]] = {}
|
||||
"""A dictionary containing a collection of developer-defined game option presets."""
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -215,23 +205,9 @@ class World(metaclass=AutoWorldRegister):
|
||||
item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||
"""maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}"""
|
||||
|
||||
item_descriptions: ClassVar[Dict[str, str]] = {}
|
||||
"""An optional map from item names (or item group names) to brief descriptions for users.
|
||||
|
||||
Individual newlines and indentation will be collapsed into spaces before these descriptions are
|
||||
displayed. This may cover only a subset of items.
|
||||
"""
|
||||
|
||||
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
|
||||
|
||||
location_descriptions: ClassVar[Dict[str, str]] = {}
|
||||
"""An optional map from location names (or location group names) to brief descriptions for users.
|
||||
|
||||
Individual newlines and indentation will be collapsed into spaces before these descriptions are
|
||||
displayed. This may cover only a subset of locations.
|
||||
"""
|
||||
|
||||
data_version: ClassVar[int] = 0
|
||||
"""
|
||||
Increment this every time something in your world's names/id mappings changes.
|
||||
@@ -486,17 +462,3 @@ def data_package_checksum(data: "GamesPackage") -> str:
|
||||
assert sorted(data) == list(data), "Data not ordered"
|
||||
from NetUtils import encode
|
||||
return hashlib.sha1(encode(data).encode()).hexdigest()
|
||||
|
||||
|
||||
def _normalize_description(description):
|
||||
"""Normalizes a description in item_descriptions or location_descriptions.
|
||||
|
||||
This allows authors to write descritions with nice indentation and line lengths in their world
|
||||
definitions without having it affect the rendered format.
|
||||
"""
|
||||
# First, collapse the whitespace around newlines and the ends of the description.
|
||||
description = re.sub(r' *\n *', '\n', description.strip())
|
||||
# Next, condense individual newlines into spaces.
|
||||
description = re.sub(r'(?<!\n)\n(?!\n)', ' ', description)
|
||||
return description
|
||||
|
||||
|
||||
@@ -2,18 +2,11 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
import os
|
||||
import threading
|
||||
|
||||
from typing import ClassVar, Dict, Tuple, Any, Optional, Union, BinaryIO
|
||||
|
||||
import bsdiff4
|
||||
|
||||
semaphore = threading.Semaphore(os.cpu_count() or 4)
|
||||
|
||||
del threading
|
||||
del os
|
||||
|
||||
|
||||
class AutoPatchRegister(type):
|
||||
patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {}
|
||||
@@ -64,12 +57,11 @@ class APContainer:
|
||||
zip_file = file if file else self.path
|
||||
if not zip_file:
|
||||
raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.")
|
||||
with semaphore: # TODO: remove semaphore once generate_output has a thread limit
|
||||
with zipfile.ZipFile(
|
||||
zip_file, "w", self.compression_method, True, self.compression_level) as zf:
|
||||
if file:
|
||||
self.path = zf.filename
|
||||
self.write_contents(zf)
|
||||
with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \
|
||||
as zf:
|
||||
if file:
|
||||
self.path = zf.filename
|
||||
self.write_contents(zf)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
manifest = self.get_manifest()
|
||||
|
||||
@@ -101,6 +101,8 @@ components: List[Component] = [
|
||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
||||
# FF1
|
||||
Component('FF1 Client', 'FF1Client'),
|
||||
# Pokémon
|
||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')),
|
||||
# ChecksFinder
|
||||
@@ -112,6 +114,8 @@ components: List[Component] = [
|
||||
# Zillion
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
# Kingdom Hearts 2
|
||||
Component('KH2 Client', "KH2Client"),
|
||||
|
||||
#MegaMan Battle Network 3
|
||||
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3'))
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
import zipimport
|
||||
from typing import Dict, List, NamedTuple, TypedDict
|
||||
|
||||
from Utils import local_path, user_path
|
||||
from Utils import user_path, local_path
|
||||
|
||||
local_folder = os.path.dirname(__file__)
|
||||
user_folder = user_path("worlds") if user_path() != local_path() else None
|
||||
|
||||
__all__ = {
|
||||
__all__ = (
|
||||
"lookup_any_item_id_to_name",
|
||||
"lookup_any_location_id_to_name",
|
||||
"network_data_package",
|
||||
"AutoWorldRegister",
|
||||
"world_sources",
|
||||
"local_folder",
|
||||
"user_folder",
|
||||
"GamesPackage",
|
||||
"DataPackage",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GamesPackage(TypedDict, total=False):
|
||||
item_name_groups: Dict[str, List[str]]
|
||||
item_name_to_id: Dict[str, int]
|
||||
location_name_groups: Dict[str, List[str]]
|
||||
location_name_to_id: Dict[str, int]
|
||||
class GamesData(typing.TypedDict):
|
||||
item_name_groups: typing.Dict[str, typing.List[str]]
|
||||
item_name_to_id: typing.Dict[str, int]
|
||||
location_name_groups: typing.Dict[str, typing.List[str]]
|
||||
location_name_to_id: typing.Dict[str, int]
|
||||
version: int
|
||||
|
||||
|
||||
class GamesPackage(GamesData, total=False):
|
||||
checksum: str
|
||||
version: int # TODO: Remove support after per game data packages API change.
|
||||
|
||||
|
||||
class DataPackage(TypedDict):
|
||||
games: Dict[str, GamesPackage]
|
||||
class DataPackage(typing.TypedDict):
|
||||
games: typing.Dict[str, GamesPackage]
|
||||
|
||||
|
||||
class WorldSource(NamedTuple):
|
||||
class WorldSource(typing.NamedTuple):
|
||||
path: str # typically relative path from this module
|
||||
is_zip: bool = False
|
||||
relative: bool = True # relative to regular world import folder
|
||||
@@ -85,7 +88,7 @@ class WorldSource(NamedTuple):
|
||||
|
||||
|
||||
# find potential world containers, currently folders and zip-importable .apworld's
|
||||
world_sources: List[WorldSource] = []
|
||||
world_sources: typing.List[WorldSource] = []
|
||||
for folder in (folder for folder in (user_folder, local_folder) if folder):
|
||||
relative = folder == local_folder
|
||||
for entry in os.scandir(folder):
|
||||
@@ -102,9 +105,25 @@ world_sources.sort()
|
||||
for world_source in world_sources:
|
||||
world_source.load()
|
||||
|
||||
lookup_any_item_id_to_name = {}
|
||||
lookup_any_location_id_to_name = {}
|
||||
games: typing.Dict[str, GamesPackage] = {}
|
||||
|
||||
from .AutoWorld import AutoWorldRegister # noqa: E402
|
||||
|
||||
# Build the data package for each game.
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
for world_name, world in AutoWorldRegister.world_types.items():
|
||||
games[world_name] = world.get_data_package_data()
|
||||
lookup_any_item_id_to_name.update(world.item_id_to_name)
|
||||
lookup_any_location_id_to_name.update(world.location_id_to_name)
|
||||
|
||||
network_data_package: DataPackage = {
|
||||
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
|
||||
"games": games,
|
||||
}
|
||||
|
||||
# Set entire datapackage to version 0 if any of them are set to 0
|
||||
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
||||
import logging
|
||||
|
||||
logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
|
||||
f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")
|
||||
|
||||
@@ -9,12 +9,10 @@ import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import json
|
||||
import sys
|
||||
import typing
|
||||
|
||||
|
||||
BIZHAWK_SOCKET_PORT_RANGE_START = 43055
|
||||
BIZHAWK_SOCKET_PORT_RANGE_SIZE = 5
|
||||
BIZHAWK_SOCKET_PORT = 43055
|
||||
|
||||
|
||||
class ConnectionStatus(enum.IntEnum):
|
||||
@@ -47,13 +45,11 @@ class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
_lock: asyncio.Lock
|
||||
_port: typing.Optional[int]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
self._lock = asyncio.Lock()
|
||||
self._port = None
|
||||
|
||||
async def _send_message(self, message: str):
|
||||
async with self._lock:
|
||||
@@ -90,24 +86,15 @@ class BizHawkContext:
|
||||
|
||||
|
||||
async def connect(ctx: BizHawkContext) -> bool:
|
||||
"""Attempts to establish a connection with a connector script. Returns True if successful."""
|
||||
rotation_steps = 0 if ctx._port is None else ctx._port - BIZHAWK_SOCKET_PORT_RANGE_START
|
||||
ports = [*range(BIZHAWK_SOCKET_PORT_RANGE_START, BIZHAWK_SOCKET_PORT_RANGE_START + BIZHAWK_SOCKET_PORT_RANGE_SIZE)]
|
||||
ports = ports[rotation_steps:] + ports[:rotation_steps]
|
||||
|
||||
for port in ports:
|
||||
try:
|
||||
ctx.streams = await asyncio.open_connection("localhost", port)
|
||||
ctx.connection_status = ConnectionStatus.TENTATIVE
|
||||
ctx._port = port
|
||||
return True
|
||||
except (TimeoutError, ConnectionRefusedError):
|
||||
continue
|
||||
|
||||
# No ports worked
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
return False
|
||||
"""Attempts to establish a connection with the connector script. Returns True if successful."""
|
||||
try:
|
||||
ctx.streams = await asyncio.open_connection("localhost", BIZHAWK_SOCKET_PORT)
|
||||
ctx.connection_status = ConnectionStatus.TENTATIVE
|
||||
return True
|
||||
except (TimeoutError, ConnectionRefusedError):
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
return False
|
||||
|
||||
|
||||
def disconnect(ctx: BizHawkContext) -> None:
|
||||
@@ -126,20 +113,7 @@ async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[s
|
||||
"""Sends a list of requests to the BizHawk connector and returns their responses.
|
||||
|
||||
It's likely you want to use the wrapper functions instead of this."""
|
||||
responses = json.loads(await ctx._send_message(json.dumps(req_list)))
|
||||
errors: typing.List[ConnectorError] = []
|
||||
|
||||
for response in responses:
|
||||
if response["type"] == "ERROR":
|
||||
errors.append(ConnectorError(response["err"]))
|
||||
|
||||
if errors:
|
||||
if sys.version_info >= (3, 11, 0):
|
||||
raise ExceptionGroup("Connector script returned errors", errors) # noqa
|
||||
else:
|
||||
raise errors[0]
|
||||
|
||||
return responses
|
||||
return json.loads(await ctx._send_message(json.dumps(req_list)))
|
||||
|
||||
|
||||
async def ping(ctx: BizHawkContext) -> None:
|
||||
@@ -259,7 +233,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[
|
||||
return None
|
||||
else:
|
||||
if item["type"] != "READ_RESPONSE":
|
||||
raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {item['type']}")
|
||||
raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {res['type']}")
|
||||
|
||||
ret.append(base64.b64decode(item["value"]))
|
||||
|
||||
@@ -311,7 +285,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl
|
||||
return False
|
||||
else:
|
||||
if item["type"] != "WRITE_RESPONSE":
|
||||
raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {item['type']}")
|
||||
raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {res['type']}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -130,18 +130,7 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
logger.info("Waiting to connect to BizHawk...")
|
||||
showed_connecting_message = True
|
||||
|
||||
# Since a call to `connect` can take a while to return, this will cancel connecting
|
||||
# if the user has decided to close the client.
|
||||
connect_task = asyncio.create_task(connect(ctx.bizhawk_ctx), name="BizHawkConnect")
|
||||
exit_task = asyncio.create_task(ctx.exit_event.wait(), name="ExitWait")
|
||||
await asyncio.wait([connect_task, exit_task], return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
if exit_task.done():
|
||||
connect_task.cancel()
|
||||
return
|
||||
|
||||
if not connect_task.result():
|
||||
# Failed to connect
|
||||
if not await connect(ctx.bizhawk_ctx):
|
||||
continue
|
||||
|
||||
showed_no_handler_message = False
|
||||
|
||||
@@ -313,6 +313,7 @@ def generate_itempool(world):
|
||||
for location_name, event_name in event_pairs:
|
||||
location = multiworld.get_location(location_name, player)
|
||||
event = ItemFactory(event_name, player)
|
||||
event.code = None
|
||||
multiworld.push_item(location, event, False)
|
||||
location.event = location.locked = True
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, AllowCollect, StartInventoryPool, \
|
||||
PlandoBosses
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -103,10 +102,9 @@ class map_shuffle(DungeonItem):
|
||||
|
||||
|
||||
class key_drop_shuffle(Toggle):
|
||||
"""Shuffle keys found in pots and dropped from killed enemies,
|
||||
respects the small key and big key shuffle options."""
|
||||
"""Shuffle keys found in pots and dropped from killed enemies."""
|
||||
display_name = "Key Drop Shuffle"
|
||||
|
||||
default = False
|
||||
|
||||
class Crystals(Range):
|
||||
range_start = 0
|
||||
@@ -427,6 +425,12 @@ class BeemizerTrapChance(BeemizerRange):
|
||||
display_name = "Beemizer Trap Chance"
|
||||
|
||||
|
||||
class AllowCollect(Toggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
Off by default, because it currently crashes on real hardware."""
|
||||
display_name = "Allow Collection of checks for other players"
|
||||
|
||||
|
||||
alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"crystals_needed_for_gt": CrystalsTower,
|
||||
"crystals_needed_for_ganon": CrystalsGanon,
|
||||
|
||||
@@ -447,7 +447,6 @@ def mark_light_world_regions(world, player: int):
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
|
||||
|
||||
old_location_address_to_new_location_address = {
|
||||
0x2eb18: 0x18001b, # Bottle Merchant
|
||||
0x33d68: 0x18001a, # Purple Chest
|
||||
|
||||
@@ -783,7 +783,6 @@ def get_nonnative_item_sprite(code: int) -> int:
|
||||
|
||||
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = world.per_slot_randoms[player]
|
||||
local_world = world.worlds[player]
|
||||
|
||||
# patch items
|
||||
|
||||
@@ -1191,8 +1190,12 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
])
|
||||
|
||||
# set Fountain bottle exchange items
|
||||
rom.write_byte(0x348FF, item_table[local_world.waterfall_fairy_bottle_fill].item_code)
|
||||
rom.write_byte(0x3493B, item_table[local_world.pyramid_fairy_bottle_fill].item_code)
|
||||
if world.difficulty[player] in ['hard', 'expert']:
|
||||
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][local_random.randint(0, 5)])
|
||||
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][local_random.randint(0, 5)])
|
||||
else:
|
||||
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][local_random.randint(0, 6)])
|
||||
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][local_random.randint(0, 6)])
|
||||
|
||||
# enable Fat Fairy Chests
|
||||
rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9])
|
||||
|
||||
@@ -31,7 +31,7 @@ def can_shoot_arrows(state: CollectionState, player: int) -> bool:
|
||||
|
||||
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
|
||||
count = state.multiworld.treasure_hunt_count[player]
|
||||
return state.count('Triforce Piece', player) + state.count('Power Star', player) >= count
|
||||
return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count
|
||||
|
||||
|
||||
def has_crystals(state: CollectionState, count: int, player: int) -> bool:
|
||||
@@ -60,9 +60,9 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int:
|
||||
def heart_count(state: CollectionState, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = state.multiworld.difficulty_requirements[player]
|
||||
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.item_count('Sanctuary Heart Container', player) \
|
||||
+ min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
+ 3 # starting hearts
|
||||
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ class ALTTPWorld(World):
|
||||
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left",
|
||||
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
|
||||
"Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right",
|
||||
"Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Right",
|
||||
"Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left",
|
||||
"Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right",
|
||||
"Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right",
|
||||
"Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room",
|
||||
@@ -249,8 +249,6 @@ class ALTTPWorld(World):
|
||||
rom_name_available_event: threading.Event
|
||||
has_progressive_bows: bool
|
||||
dungeons: typing.Dict[str, Dungeon]
|
||||
waterfall_fairy_bottle_fill: str
|
||||
pyramid_fairy_bottle_fill: str
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
@@ -258,8 +256,6 @@ class ALTTPWorld(World):
|
||||
self.rom_name_available_event = threading.Event()
|
||||
self.has_progressive_bows = False
|
||||
self.dungeons = {}
|
||||
self.waterfall_fairy_bottle_fill = "Bottle"
|
||||
self.pyramid_fairy_bottle_fill = "Bottle"
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -277,62 +273,52 @@ class ALTTPWorld(World):
|
||||
def generate_early(self):
|
||||
|
||||
player = self.player
|
||||
multiworld = self.multiworld
|
||||
world = self.multiworld
|
||||
|
||||
# fairy bottle fills
|
||||
bottle_options = [
|
||||
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
|
||||
"Bottle (Bee)", "Bottle (Good Bee)"
|
||||
]
|
||||
if multiworld.difficulty[player] not in ["hard", "expert"]:
|
||||
bottle_options.append("Bottle (Fairy)")
|
||||
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
|
||||
if multiworld.mode[player] == 'standard' \
|
||||
and multiworld.smallkey_shuffle[player] \
|
||||
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \
|
||||
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \
|
||||
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with:
|
||||
if world.mode[player] == 'standard' \
|
||||
and world.smallkey_shuffle[player] \
|
||||
and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal \
|
||||
and world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \
|
||||
and world.smallkey_shuffle[player] != smallkey_shuffle.option_start_with:
|
||||
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
||||
|
||||
# system for sharing ER layouts
|
||||
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
|
||||
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in multiworld.shuffle[player]:
|
||||
shuffle, seed = multiworld.shuffle[player].split("-", 1)
|
||||
multiworld.shuffle[player] = shuffle
|
||||
if "-" in world.shuffle[player]:
|
||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
self.er_seed = "vanilla"
|
||||
elif seed.startswith("group-") or multiworld.is_race:
|
||||
self.er_seed = get_same_seed(multiworld, (
|
||||
shuffle, seed, multiworld.retro_caves[player], multiworld.mode[player], multiworld.logic[player]))
|
||||
elif seed.startswith("group-") or world.is_race:
|
||||
self.er_seed = get_same_seed(world, (
|
||||
shuffle, seed, world.retro_caves[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
self.er_seed = seed
|
||||
elif multiworld.shuffle[player] == "vanilla":
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
self.er_seed = "vanilla"
|
||||
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
|
||||
option = getattr(multiworld, dungeon_item)[player]
|
||||
option = getattr(world, dungeon_item)[player]
|
||||
if option == "own_world":
|
||||
multiworld.local_items[player].value |= self.item_name_groups[option.item_name_group]
|
||||
world.local_items[player].value |= self.item_name_groups[option.item_name_group]
|
||||
elif option == "different_world":
|
||||
multiworld.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
|
||||
if multiworld.mode[player] == "standard":
|
||||
multiworld.non_local_items[player].value -= {"Small Key (Hyrule Castle)"}
|
||||
world.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
|
||||
if world.mode[player] == "standard":
|
||||
world.non_local_items[player].value -= {"Small Key (Hyrule Castle)"}
|
||||
elif option.in_dungeon:
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "original_dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
|
||||
multiworld.difficulty_requirements[player] = difficulties[multiworld.difficulty[player]]
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
# enforce pre-defined local items.
|
||||
if multiworld.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
multiworld.local_items[player].value.add('Triforce Piece')
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
world.local_items[player].value.add('Triforce Piece')
|
||||
|
||||
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
|
||||
multiworld.non_local_items[player].value -= item_name_groups['Pendants']
|
||||
multiworld.non_local_items[player].value -= item_name_groups['Crystals']
|
||||
world.non_local_items[player].value -= item_name_groups['Pendants']
|
||||
world.non_local_items[player].value -= item_name_groups['Crystals']
|
||||
|
||||
create_dungeons = create_dungeons
|
||||
|
||||
@@ -378,6 +364,7 @@ class ALTTPWorld(World):
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item, remove=False):
|
||||
item_name = item.name
|
||||
if item_name.startswith('Progressive '):
|
||||
@@ -706,18 +693,13 @@ class ALTTPWorld(World):
|
||||
spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player])
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||
player_name = self.multiworld.get_player_name(self.player)
|
||||
spoiler_handle.write("\n\nMedallions:\n")
|
||||
spoiler_handle.write(f"\nMisery Mire ({player_name}):"
|
||||
spoiler_handle.write(f"\nMisery Mire ({self.multiworld.get_player_name(self.player)}):"
|
||||
f" {self.multiworld.required_medallions[self.player][0]}")
|
||||
spoiler_handle.write(
|
||||
f"\nTurtle Rock ({player_name}):"
|
||||
f"\nTurtle Rock ({self.multiworld.get_player_name(self.player)}):"
|
||||
f" {self.multiworld.required_medallions[self.player][1]}")
|
||||
spoiler_handle.write("\n\nFairy Fountain Bottle Fill:\n")
|
||||
spoiler_handle.write(f"\nPyramid Fairy ({player_name}):"
|
||||
f" {self.pyramid_fairy_bottle_fill}")
|
||||
spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):"
|
||||
f" {self.waterfall_fairy_bottle_fill}")
|
||||
|
||||
if self.multiworld.boss_shuffle[self.player] != "none":
|
||||
def create_boss_map() -> typing.Dict:
|
||||
boss_map = {
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
from ..generic.Rules import set_rule
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from ..generic.Rules import set_rule, add_rule
|
||||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
|
||||
|
||||
def _has_total(state: CollectionState, player: int, total: int):
|
||||
return (state.count('Map Width', player) + state.count('Map Height', player) +
|
||||
state.count('Map Bombs', player)) >= total
|
||||
class ChecksFinderLogic(LogicMixin):
|
||||
|
||||
def _has_total(self, player: int, total: int):
|
||||
return (self.item_count('Map Width', player)+self.item_count('Map Height', player)+
|
||||
self.item_count('Map Bombs', player)) >= total
|
||||
|
||||
|
||||
# Sets rules on entrances and advancements that are always applied
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1))
|
||||
set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2))
|
||||
set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3))
|
||||
set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4))
|
||||
set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5))
|
||||
set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6))
|
||||
set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7))
|
||||
set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8))
|
||||
set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9))
|
||||
set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10))
|
||||
set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11))
|
||||
set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12))
|
||||
set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13))
|
||||
set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14))
|
||||
set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15))
|
||||
set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16))
|
||||
set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17))
|
||||
set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18))
|
||||
set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19))
|
||||
set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20))
|
||||
set_rule(world.get_location(("Tile 6"), player), lambda state: state._has_total(player, 1))
|
||||
set_rule(world.get_location(("Tile 7"), player), lambda state: state._has_total(player, 2))
|
||||
set_rule(world.get_location(("Tile 8"), player), lambda state: state._has_total(player, 3))
|
||||
set_rule(world.get_location(("Tile 9"), player), lambda state: state._has_total(player, 4))
|
||||
set_rule(world.get_location(("Tile 10"), player), lambda state: state._has_total(player, 5))
|
||||
set_rule(world.get_location(("Tile 11"), player), lambda state: state._has_total(player, 6))
|
||||
set_rule(world.get_location(("Tile 12"), player), lambda state: state._has_total(player, 7))
|
||||
set_rule(world.get_location(("Tile 13"), player), lambda state: state._has_total(player, 8))
|
||||
set_rule(world.get_location(("Tile 14"), player), lambda state: state._has_total(player, 9))
|
||||
set_rule(world.get_location(("Tile 15"), player), lambda state: state._has_total(player, 10))
|
||||
set_rule(world.get_location(("Tile 16"), player), lambda state: state._has_total(player, 11))
|
||||
set_rule(world.get_location(("Tile 17"), player), lambda state: state._has_total(player, 12))
|
||||
set_rule(world.get_location(("Tile 18"), player), lambda state: state._has_total(player, 13))
|
||||
set_rule(world.get_location(("Tile 19"), player), lambda state: state._has_total(player, 14))
|
||||
set_rule(world.get_location(("Tile 20"), player), lambda state: state._has_total(player, 15))
|
||||
set_rule(world.get_location(("Tile 21"), player), lambda state: state._has_total(player, 16))
|
||||
set_rule(world.get_location(("Tile 22"), player), lambda state: state._has_total(player, 17))
|
||||
set_rule(world.get_location(("Tile 23"), player), lambda state: state._has_total(player, 18))
|
||||
set_rule(world.get_location(("Tile 24"), player), lambda state: state._has_total(player, 19))
|
||||
set_rule(world.get_location(("Tile 25"), player), lambda state: state._has_total(player, 20))
|
||||
|
||||
|
||||
# Sets rules on completion condition
|
||||
|
||||
@@ -14,8 +14,8 @@ class ChecksFinderWeb(WebWorld):
|
||||
"A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers "
|
||||
"single-player, multiworld, and related software.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
"checksfinder_en.md",
|
||||
"checksfinder/en",
|
||||
["Mewlif"]
|
||||
)]
|
||||
|
||||
|
||||
@@ -1271,14 +1271,6 @@ _cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [
|
||||
("Dorris Swarm", 0x40393870, DS3ItemCategory.SKIP),
|
||||
]]
|
||||
|
||||
item_descriptions = {
|
||||
"Cinders": """
|
||||
All four Cinders of a Lord.
|
||||
|
||||
Once you have these four, you can fight Soul of Cinder and win the game.
|
||||
""",
|
||||
}
|
||||
|
||||
_all_items = _vanilla_items + _dlc_items
|
||||
|
||||
item_dictionary = {item_data.name: item_data for item_data in _all_items}
|
||||
|
||||
@@ -171,16 +171,6 @@ class MaxLevelsIn10WeaponPoolOption(Range):
|
||||
default = 10
|
||||
|
||||
|
||||
class EarlySmallLothricBanner(Choice):
|
||||
"""This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or
|
||||
into an early sphere across all worlds."""
|
||||
display_name = "Early Small Lothric Banner"
|
||||
option_off = 0
|
||||
option_early_global = 1
|
||||
option_early_local = 2
|
||||
default = option_off
|
||||
|
||||
|
||||
class LateBasinOfVowsOption(Toggle):
|
||||
"""This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into
|
||||
Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early,
|
||||
@@ -225,7 +215,6 @@ dark_souls_options: typing.Dict[str, Option] = {
|
||||
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
|
||||
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
|
||||
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
|
||||
"early_banner": EarlySmallLothricBanner,
|
||||
"late_basin_of_vows": LateBasinOfVowsOption,
|
||||
"late_dlc": LateDLCOption,
|
||||
"no_spell_requirements": NoSpellRequirementsOption,
|
||||
|
||||
@@ -7,9 +7,9 @@ from Options import Toggle
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule
|
||||
|
||||
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions
|
||||
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names
|
||||
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options
|
||||
|
||||
|
||||
class DarkSouls3Web(WebWorld):
|
||||
@@ -60,7 +60,6 @@ class DarkSouls3World(World):
|
||||
"Cinders of a Lord - Lothric Prince"
|
||||
}
|
||||
}
|
||||
item_descriptions = item_descriptions
|
||||
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
@@ -86,10 +85,6 @@ class DarkSouls3World(World):
|
||||
self.enabled_location_categories.add(DS3LocationCategory.NPC)
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.KEY)
|
||||
if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global:
|
||||
self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1
|
||||
elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local:
|
||||
self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, DeathLink, NamedRange, PerGameCommonOptions
|
||||
from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange
|
||||
|
||||
|
||||
class DoubleJumpGlitch(Choice):
|
||||
@@ -33,7 +33,7 @@ class CoinSanity(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class CoinSanityRange(NamedRange):
|
||||
class CoinSanityRange(SpecialRange):
|
||||
"""This is the amount of coins in a coin bundle
|
||||
You need to collect that number of coins to get a location check, and when receiving coin items, you will get bundles of this size
|
||||
It is highly recommended to not set this value below 10, as it generates a very large number of boring locations and items.
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Union
|
||||
from BaseClasses import Tutorial, CollectionState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Options
|
||||
from .Items import DLCQuestItem, ItemData, create_items, item_table, items_by_group, Group
|
||||
from .Items import DLCQuestItem, ItemData, create_items, item_table
|
||||
from .Locations import DLCQuestLocation, location_table
|
||||
from .Options import DLCQuestOptions
|
||||
from .Regions import create_regions
|
||||
@@ -60,9 +60,7 @@ class DLCqworld(World):
|
||||
created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random)
|
||||
|
||||
self.multiworld.itempool += created_items
|
||||
|
||||
if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both:
|
||||
self.multiworld.early_items[self.player]["Movement Pack"] = 1
|
||||
self.multiworld.early_items[self.player]["Movement Pack"] = 1
|
||||
|
||||
for item in items_to_exclude:
|
||||
if item in self.multiworld.itempool:
|
||||
@@ -79,10 +77,6 @@ class DLCqworld(World):
|
||||
|
||||
return DLCQuestItem(item.name, item.classification, item.code, self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
trap = self.multiworld.random.choice(items_by_group[Group.Trap])
|
||||
return trap.name
|
||||
|
||||
def fill_slot_data(self):
|
||||
options_dict = self.options.as_dict(
|
||||
"death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import NamedRange
|
||||
from Options import SpecialRange
|
||||
from .option_names import options_to_include
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
|
||||
@@ -14,7 +14,7 @@ def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
|
||||
|
||||
def get_option_choices(option) -> Dict[str, int]:
|
||||
if issubclass(option, NamedRange):
|
||||
if issubclass(option, SpecialRange):
|
||||
return option.special_range_names
|
||||
elif option.options:
|
||||
return option.options
|
||||
|
||||
@@ -1165,7 +1165,6 @@ item_table: Dict[int, ItemDict] = {
|
||||
|
||||
item_name_groups: Dict[str, Set[str]] = {
|
||||
'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', },
|
||||
'Computer area maps': {'Against Thee Wickedly (E4M6) - Computer area map', 'And Hell Followed (E4M7) - Computer area map', 'Central Processing (E1M6) - Computer area map', 'Command Center (E2M5) - Computer area map', 'Command Control (E1M4) - Computer area map', 'Computer Station (E1M7) - Computer area map', 'Containment Area (E2M2) - Computer area map', 'Deimos Anomaly (E2M1) - Computer area map', 'Deimos Lab (E2M4) - Computer area map', 'Dis (E3M8) - Computer area map', 'Fear (E4M9) - Computer area map', 'Fortress of Mystery (E2M9) - Computer area map', 'Halls of the Damned (E2M6) - Computer area map', 'Hangar (E1M1) - Computer area map', 'Hell Beneath (E4M1) - Computer area map', 'Hell Keep (E3M1) - Computer area map', 'House of Pain (E3M4) - Computer area map', 'Limbo (E3M7) - Computer area map', 'Military Base (E1M9) - Computer area map', 'Mt. Erebus (E3M6) - Computer area map', 'Nuclear Plant (E1M2) - Computer area map', 'Pandemonium (E3M3) - Computer area map', 'Perfect Hatred (E4M2) - Computer area map', 'Phobos Anomaly (E1M8) - Computer area map', 'Phobos Lab (E1M5) - Computer area map', 'Refinery (E2M3) - Computer area map', 'Sever the Wicked (E4M3) - Computer area map', 'Slough of Despair (E3M2) - Computer area map', 'Spawning Vats (E2M7) - Computer area map', 'They Will Repent (E4M5) - Computer area map', 'Tower of Babel (E2M8) - Computer area map', 'Toxin Refinery (E1M3) - Computer area map', 'Unholy Cathedral (E3M5) - Computer area map', 'Unruly Evil (E4M4) - Computer area map', 'Unto the Cruel (E4M8) - Computer area map', 'Warrens (E3M9) - Computer area map', },
|
||||
'Keys': {'Against Thee Wickedly (E4M6) - Blue skull key', 'Against Thee Wickedly (E4M6) - Red skull key', 'Against Thee Wickedly (E4M6) - Yellow skull key', 'And Hell Followed (E4M7) - Blue skull key', 'And Hell Followed (E4M7) - Red skull key', 'And Hell Followed (E4M7) - Yellow skull key', 'Central Processing (E1M6) - Blue keycard', 'Central Processing (E1M6) - Red keycard', 'Central Processing (E1M6) - Yellow keycard', 'Command Control (E1M4) - Blue keycard', 'Command Control (E1M4) - Yellow keycard', 'Computer Station (E1M7) - Blue keycard', 'Computer Station (E1M7) - Red keycard', 'Computer Station (E1M7) - Yellow keycard', 'Containment Area (E2M2) - Blue keycard', 'Containment Area (E2M2) - Red keycard', 'Containment Area (E2M2) - Yellow keycard', 'Deimos Anomaly (E2M1) - Blue keycard', 'Deimos Anomaly (E2M1) - Red keycard', 'Deimos Lab (E2M4) - Blue keycard', 'Deimos Lab (E2M4) - Yellow keycard', 'Fear (E4M9) - Yellow skull key', 'Fortress of Mystery (E2M9) - Blue skull key', 'Fortress of Mystery (E2M9) - Red skull key', 'Fortress of Mystery (E2M9) - Yellow skull key', 'Halls of the Damned (E2M6) - Blue skull key', 'Halls of the Damned (E2M6) - Red skull key', 'Halls of the Damned (E2M6) - Yellow skull key', 'Hell Beneath (E4M1) - Blue skull key', 'Hell Beneath (E4M1) - Red skull key', 'House of Pain (E3M4) - Blue skull key', 'House of Pain (E3M4) - Red skull key', 'House of Pain (E3M4) - Yellow skull key', 'Limbo (E3M7) - Blue skull key', 'Limbo (E3M7) - Red skull key', 'Limbo (E3M7) - Yellow skull key', 'Military Base (E1M9) - Blue keycard', 'Military Base (E1M9) - Red keycard', 'Military Base (E1M9) - Yellow keycard', 'Mt. Erebus (E3M6) - Blue skull key', 'Nuclear Plant (E1M2) - Red keycard', 'Pandemonium (E3M3) - Blue skull key', 'Perfect Hatred (E4M2) - Blue skull key', 'Perfect Hatred (E4M2) - Yellow skull key', 'Phobos Lab (E1M5) - Blue keycard', 'Phobos Lab (E1M5) - Yellow keycard', 'Refinery (E2M3) - Blue keycard', 'Sever the Wicked (E4M3) - Blue skull key', 'Sever the Wicked (E4M3) - Red skull key', 'Slough of Despair (E3M2) - Blue skull key', 'Spawning Vats (E2M7) - Blue keycard', 'Spawning Vats (E2M7) - Red keycard', 'Spawning Vats (E2M7) - Yellow keycard', 'They Will Repent (E4M5) - Blue skull key', 'They Will Repent (E4M5) - Red skull key', 'They Will Repent (E4M5) - Yellow skull key', 'Toxin Refinery (E1M3) - Blue keycard', 'Toxin Refinery (E1M3) - Yellow keycard', 'Unholy Cathedral (E3M5) - Blue skull key', 'Unholy Cathedral (E3M5) - Yellow skull key', 'Unruly Evil (E4M4) - Red skull key', 'Unto the Cruel (E4M8) - Red skull key', 'Unto the Cruel (E4M8) - Yellow skull key', 'Warrens (E3M9) - Blue skull key', 'Warrens (E3M9) - Red skull key', },
|
||||
'Levels': {'Against Thee Wickedly (E4M6)', 'And Hell Followed (E4M7)', 'Central Processing (E1M6)', 'Command Center (E2M5)', 'Command Control (E1M4)', 'Computer Station (E1M7)', 'Containment Area (E2M2)', 'Deimos Anomaly (E2M1)', 'Deimos Lab (E2M4)', 'Dis (E3M8)', 'Fear (E4M9)', 'Fortress of Mystery (E2M9)', 'Halls of the Damned (E2M6)', 'Hangar (E1M1)', 'Hell Beneath (E4M1)', 'Hell Keep (E3M1)', 'House of Pain (E3M4)', 'Limbo (E3M7)', 'Military Base (E1M9)', 'Mt. Erebus (E3M6)', 'Nuclear Plant (E1M2)', 'Pandemonium (E3M3)', 'Perfect Hatred (E4M2)', 'Phobos Anomaly (E1M8)', 'Phobos Lab (E1M5)', 'Refinery (E2M3)', 'Sever the Wicked (E4M3)', 'Slough of Despair (E3M2)', 'Spawning Vats (E2M7)', 'They Will Repent (E4M5)', 'Tower of Babel (E2M8)', 'Toxin Refinery (E1M3)', 'Unholy Cathedral (E3M5)', 'Unruly Evil (E4M4)', 'Unto the Cruel (E4M8)', 'Warrens (E3M9)', },
|
||||
'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Partial invisibility', 'Supercharge', },
|
||||
|
||||
@@ -1968,7 +1968,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 2,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Containment Area (E2M2) Red Exit"},
|
||||
'region': "Containment Area (E2M2) Red"},
|
||||
351326: {'name': 'Deimos Anomaly (E2M1) - Exit',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
import typing
|
||||
|
||||
from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""
|
||||
Choose the main goal.
|
||||
complete_all_levels: All levels of the selected episodes
|
||||
complete_boss_levels: Boss levels (E#M8) of selected episodes
|
||||
"""
|
||||
display_name = "Goal"
|
||||
option_complete_all_levels = 0
|
||||
option_complete_boss_levels = 1
|
||||
default = 0
|
||||
from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
@@ -39,13 +27,11 @@ class RandomMonsters(Choice):
|
||||
vanilla: No randomization
|
||||
shuffle: Monsters are shuffled within the level
|
||||
random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big)
|
||||
random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game.
|
||||
"""
|
||||
display_name = "Random Monsters"
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_random_balanced = 2
|
||||
option_random_chaotic = 3
|
||||
default = 1
|
||||
|
||||
|
||||
@@ -63,34 +49,6 @@ class RandomPickups(Choice):
|
||||
default = 1
|
||||
|
||||
|
||||
class RandomMusic(Choice):
|
||||
"""
|
||||
Level musics will be randomized.
|
||||
vanilla: No randomization
|
||||
shuffle_selected: Selected episodes' levels will be shuffled
|
||||
shuffle_game: All the music will be shuffled
|
||||
"""
|
||||
display_name = "Random Music"
|
||||
option_vanilla = 0
|
||||
option_shuffle_selected = 1
|
||||
option_shuffle_game = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class FlipLevels(Choice):
|
||||
"""
|
||||
Flip levels on one axis.
|
||||
vanilla: No flipping
|
||||
flipped: All levels are flipped
|
||||
randomly_flipped: Random levels are flipped
|
||||
"""
|
||||
display_name = "Flip Levels"
|
||||
option_vanilla = 0
|
||||
option_flipped = 1
|
||||
option_randomly_flipped = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class AllowDeathLogic(Toggle):
|
||||
"""Some locations require a timed puzzle that can only be tried once.
|
||||
After which, if the player failed to get it, the location cannot be checked anymore.
|
||||
@@ -98,24 +56,12 @@ class AllowDeathLogic(Toggle):
|
||||
Get killed in the current map. The map will reset, you can now attempt the puzzle again."""
|
||||
display_name = "Allow Death Logic"
|
||||
|
||||
|
||||
class Pro(Toggle):
|
||||
"""Include difficult tricks into rules. Mostly employed by speed runners.
|
||||
i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc."""
|
||||
display_name = "Pro Doom"
|
||||
|
||||
|
||||
class StartWithComputerAreaMaps(Toggle):
|
||||
"""Give the player all Computer Area Map items from the start."""
|
||||
display_name = "Start With Computer Area Maps"
|
||||
|
||||
|
||||
class ResetLevelOnDeath(DefaultOnToggle):
|
||||
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
|
||||
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
|
||||
display_name="Reset Level on Death"
|
||||
|
||||
|
||||
class Episode1(DefaultOnToggle):
|
||||
"""Knee-Deep in the Dead.
|
||||
If none of the episodes are chosen, Episode 1 will be chosen by default."""
|
||||
@@ -141,18 +87,12 @@ class Episode4(Toggle):
|
||||
|
||||
|
||||
options: typing.Dict[str, AssembleOptions] = {
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
"goal": Goal,
|
||||
"difficulty": Difficulty,
|
||||
"random_monsters": RandomMonsters,
|
||||
"random_pickups": RandomPickups,
|
||||
"random_music": RandomMusic,
|
||||
"flip_levels": FlipLevels,
|
||||
"allow_death_logic": AllowDeathLogic,
|
||||
"pro": Pro,
|
||||
"start_with_computer_area_maps": StartWithComputerAreaMaps,
|
||||
"death_link": DeathLink,
|
||||
"reset_level_on_death": ResetLevelOnDeath,
|
||||
"episode1": Episode1,
|
||||
"episode2": Episode2,
|
||||
"episode3": Episode3,
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
from typing import List
|
||||
from BaseClasses import TypedDict
|
||||
|
||||
class ConnectionDict(TypedDict, total=False):
|
||||
target: str
|
||||
pro: bool
|
||||
|
||||
class RegionDict(TypedDict, total=False):
|
||||
class RegionDict(TypedDict, total=False):
|
||||
name: str
|
||||
connects_to_hub: bool
|
||||
episode: int
|
||||
connections: List[ConnectionDict]
|
||||
connections: List[str]
|
||||
|
||||
|
||||
regions:List[RegionDict] = [
|
||||
@@ -25,131 +21,121 @@ regions:List[RegionDict] = [
|
||||
{"name":"Nuclear Plant (E1M2) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Nuclear Plant (E1M2) Red","pro":False}]},
|
||||
"connections":["Nuclear Plant (E1M2) Red"]},
|
||||
{"name":"Nuclear Plant (E1M2) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Nuclear Plant (E1M2) Main","pro":False}]},
|
||||
"connections":["Nuclear Plant (E1M2) Main"]},
|
||||
|
||||
# Toxin Refinery (E1M3)
|
||||
{"name":"Toxin Refinery (E1M3) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
|
||||
"connections":["Toxin Refinery (E1M3) Blue"]},
|
||||
{"name":"Toxin Refinery (E1M3) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Toxin Refinery (E1M3) Yellow","pro":False},
|
||||
{"target":"Toxin Refinery (E1M3) Main","pro":False}]},
|
||||
"Toxin Refinery (E1M3) Yellow",
|
||||
"Toxin Refinery (E1M3) Main"]},
|
||||
{"name":"Toxin Refinery (E1M3) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
|
||||
"connections":["Toxin Refinery (E1M3) Blue"]},
|
||||
|
||||
# Command Control (E1M4)
|
||||
{"name":"Command Control (E1M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Command Control (E1M4) Blue","pro":False},
|
||||
{"target":"Command Control (E1M4) Yellow","pro":False},
|
||||
{"target":"Command Control (E1M4) Ledge","pro":True}]},
|
||||
"Command Control (E1M4) Blue",
|
||||
"Command Control (E1M4) Yellow"]},
|
||||
{"name":"Command Control (E1M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Command Control (E1M4) Ledge","pro":False},
|
||||
{"target":"Command Control (E1M4) Main","pro":False}]},
|
||||
"connections":["Command Control (E1M4) Main"]},
|
||||
{"name":"Command Control (E1M4) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Command Control (E1M4) Main","pro":False}]},
|
||||
{"name":"Command Control (E1M4) Ledge",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Command Control (E1M4) Main","pro":False},
|
||||
{"target":"Command Control (E1M4) Blue","pro":False},
|
||||
{"target":"Command Control (E1M4) Yellow","pro":False}]},
|
||||
"connections":["Command Control (E1M4) Main"]},
|
||||
|
||||
# Phobos Lab (E1M5)
|
||||
{"name":"Phobos Lab (E1M5) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Phobos Lab (E1M5) Yellow","pro":False}]},
|
||||
"connections":["Phobos Lab (E1M5) Yellow"]},
|
||||
{"name":"Phobos Lab (E1M5) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Phobos Lab (E1M5) Main","pro":False},
|
||||
{"target":"Phobos Lab (E1M5) Blue","pro":False},
|
||||
{"target":"Phobos Lab (E1M5) Green","pro":False}]},
|
||||
"Phobos Lab (E1M5) Main",
|
||||
"Phobos Lab (E1M5) Blue",
|
||||
"Phobos Lab (E1M5) Green"]},
|
||||
{"name":"Phobos Lab (E1M5) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Phobos Lab (E1M5) Green","pro":False},
|
||||
{"target":"Phobos Lab (E1M5) Yellow","pro":False}]},
|
||||
"Phobos Lab (E1M5) Green",
|
||||
"Phobos Lab (E1M5) Yellow"]},
|
||||
{"name":"Phobos Lab (E1M5) Green",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Phobos Lab (E1M5) Main","pro":False},
|
||||
{"target":"Phobos Lab (E1M5) Blue","pro":False}]},
|
||||
"Phobos Lab (E1M5) Main",
|
||||
"Phobos Lab (E1M5) Blue"]},
|
||||
|
||||
# Central Processing (E1M6)
|
||||
{"name":"Central Processing (E1M6) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Central Processing (E1M6) Yellow","pro":False},
|
||||
{"target":"Central Processing (E1M6) Red","pro":False},
|
||||
{"target":"Central Processing (E1M6) Blue","pro":False},
|
||||
{"target":"Central Processing (E1M6) Nukage","pro":False}]},
|
||||
"Central Processing (E1M6) Yellow",
|
||||
"Central Processing (E1M6) Red",
|
||||
"Central Processing (E1M6) Blue",
|
||||
"Central Processing (E1M6) Nukage"]},
|
||||
{"name":"Central Processing (E1M6) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Central Processing (E1M6) Main","pro":False}]},
|
||||
"connections":["Central Processing (E1M6) Main"]},
|
||||
{"name":"Central Processing (E1M6) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Central Processing (E1M6) Main","pro":False}]},
|
||||
"connections":["Central Processing (E1M6) Main"]},
|
||||
{"name":"Central Processing (E1M6) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Central Processing (E1M6) Main","pro":False}]},
|
||||
"connections":["Central Processing (E1M6) Main"]},
|
||||
{"name":"Central Processing (E1M6) Nukage",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Central Processing (E1M6) Yellow","pro":False}]},
|
||||
"connections":["Central Processing (E1M6) Yellow"]},
|
||||
|
||||
# Computer Station (E1M7)
|
||||
{"name":"Computer Station (E1M7) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Computer Station (E1M7) Red","pro":False},
|
||||
{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
"Computer Station (E1M7) Red",
|
||||
"Computer Station (E1M7) Yellow"]},
|
||||
{"name":"Computer Station (E1M7) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
"connections":["Computer Station (E1M7) Yellow"]},
|
||||
{"name":"Computer Station (E1M7) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Main","pro":False}]},
|
||||
"connections":["Computer Station (E1M7) Main"]},
|
||||
{"name":"Computer Station (E1M7) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Computer Station (E1M7) Blue","pro":False},
|
||||
{"target":"Computer Station (E1M7) Courtyard","pro":False},
|
||||
{"target":"Computer Station (E1M7) Main","pro":False}]},
|
||||
"Computer Station (E1M7) Blue",
|
||||
"Computer Station (E1M7) Courtyard",
|
||||
"Computer Station (E1M7) Main"]},
|
||||
{"name":"Computer Station (E1M7) Courtyard",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
"connections":["Computer Station (E1M7) Yellow"]},
|
||||
|
||||
# Phobos Anomaly (E1M8)
|
||||
{"name":"Phobos Anomaly (E1M8) Main",
|
||||
@@ -159,98 +145,91 @@ regions:List[RegionDict] = [
|
||||
{"name":"Phobos Anomaly (E1M8) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Phobos Anomaly (E1M8) Main","pro":False}]},
|
||||
"connections":["Phobos Anomaly (E1M8) Main"]},
|
||||
|
||||
# Military Base (E1M9)
|
||||
{"name":"Military Base (E1M9) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Military Base (E1M9) Blue","pro":False},
|
||||
{"target":"Military Base (E1M9) Yellow","pro":False},
|
||||
{"target":"Military Base (E1M9) Red","pro":False}]},
|
||||
"Military Base (E1M9) Blue",
|
||||
"Military Base (E1M9) Yellow",
|
||||
"Military Base (E1M9) Red"]},
|
||||
{"name":"Military Base (E1M9) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Military Base (E1M9) Main","pro":False}]},
|
||||
"connections":["Military Base (E1M9) Main"]},
|
||||
{"name":"Military Base (E1M9) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Military Base (E1M9) Main","pro":False}]},
|
||||
"connections":["Military Base (E1M9) Main"]},
|
||||
{"name":"Military Base (E1M9) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Military Base (E1M9) Main","pro":False}]},
|
||||
"connections":["Military Base (E1M9) Main"]},
|
||||
|
||||
# Deimos Anomaly (E2M1)
|
||||
{"name":"Deimos Anomaly (E2M1) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Deimos Anomaly (E2M1) Red","pro":False},
|
||||
{"target":"Deimos Anomaly (E2M1) Blue","pro":False}]},
|
||||
"Deimos Anomaly (E2M1) Red",
|
||||
"Deimos Anomaly (E2M1) Blue"]},
|
||||
{"name":"Deimos Anomaly (E2M1) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]},
|
||||
"connections":["Deimos Anomaly (E2M1) Main"]},
|
||||
{"name":"Deimos Anomaly (E2M1) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]},
|
||||
"connections":["Deimos Anomaly (E2M1) Main"]},
|
||||
|
||||
# Containment Area (E2M2)
|
||||
{"name":"Containment Area (E2M2) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Containment Area (E2M2) Yellow","pro":False},
|
||||
{"target":"Containment Area (E2M2) Blue","pro":False},
|
||||
{"target":"Containment Area (E2M2) Red","pro":False},
|
||||
{"target":"Containment Area (E2M2) Red Exit","pro":True}]},
|
||||
"Containment Area (E2M2) Yellow",
|
||||
"Containment Area (E2M2) Blue",
|
||||
"Containment Area (E2M2) Red"]},
|
||||
{"name":"Containment Area (E2M2) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Containment Area (E2M2) Main","pro":False}]},
|
||||
"connections":["Containment Area (E2M2) Main"]},
|
||||
{"name":"Containment Area (E2M2) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Containment Area (E2M2) Main","pro":False},
|
||||
{"target":"Containment Area (E2M2) Red Exit","pro":False}]},
|
||||
"connections":["Containment Area (E2M2) Main"]},
|
||||
{"name":"Containment Area (E2M2) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Containment Area (E2M2) Main","pro":False}]},
|
||||
{"name":"Containment Area (E2M2) Red Exit",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
"connections":["Containment Area (E2M2) Main"]},
|
||||
|
||||
# Refinery (E2M3)
|
||||
{"name":"Refinery (E2M3) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Refinery (E2M3) Blue","pro":False}]},
|
||||
"connections":["Refinery (E2M3) Blue"]},
|
||||
{"name":"Refinery (E2M3) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Refinery (E2M3) Main","pro":False}]},
|
||||
"connections":["Refinery (E2M3) Main"]},
|
||||
|
||||
# Deimos Lab (E2M4)
|
||||
{"name":"Deimos Lab (E2M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
|
||||
"connections":["Deimos Lab (E2M4) Blue"]},
|
||||
{"name":"Deimos Lab (E2M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Deimos Lab (E2M4) Main","pro":False},
|
||||
{"target":"Deimos Lab (E2M4) Yellow","pro":False}]},
|
||||
"Deimos Lab (E2M4) Main",
|
||||
"Deimos Lab (E2M4) Yellow"]},
|
||||
{"name":"Deimos Lab (E2M4) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
|
||||
"connections":["Deimos Lab (E2M4) Blue"]},
|
||||
|
||||
# Command Center (E2M5)
|
||||
{"name":"Command Center (E2M5) Main",
|
||||
@@ -263,54 +242,47 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Halls of the Damned (E2M6) Blue Yellow Red","pro":False},
|
||||
{"target":"Halls of the Damned (E2M6) Yellow","pro":False},
|
||||
{"target":"Halls of the Damned (E2M6) One way Yellow","pro":False}]},
|
||||
"Halls of the Damned (E2M6) Blue Yellow Red",
|
||||
"Halls of the Damned (E2M6) Yellow",
|
||||
"Halls of the Damned (E2M6) One way Yellow"]},
|
||||
{"name":"Halls of the Damned (E2M6) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]},
|
||||
"connections":["Halls of the Damned (E2M6) Main"]},
|
||||
{"name":"Halls of the Damned (E2M6) Blue Yellow Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]},
|
||||
"connections":["Halls of the Damned (E2M6) Main"]},
|
||||
{"name":"Halls of the Damned (E2M6) One way Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]},
|
||||
"connections":["Halls of the Damned (E2M6) Main"]},
|
||||
|
||||
# Spawning Vats (E2M7)
|
||||
{"name":"Spawning Vats (E2M7) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Spawning Vats (E2M7) Blue","pro":False},
|
||||
{"target":"Spawning Vats (E2M7) Entrance Secret","pro":False},
|
||||
{"target":"Spawning Vats (E2M7) Red","pro":False},
|
||||
{"target":"Spawning Vats (E2M7) Yellow","pro":False},
|
||||
{"target":"Spawning Vats (E2M7) Red Exit","pro":True}]},
|
||||
"Spawning Vats (E2M7) Blue",
|
||||
"Spawning Vats (E2M7) Entrance Secret",
|
||||
"Spawning Vats (E2M7) Red",
|
||||
"Spawning Vats (E2M7) Yellow"]},
|
||||
{"name":"Spawning Vats (E2M7) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]},
|
||||
"connections":["Spawning Vats (E2M7) Main"]},
|
||||
{"name":"Spawning Vats (E2M7) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]},
|
||||
"connections":["Spawning Vats (E2M7) Main"]},
|
||||
{"name":"Spawning Vats (E2M7) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Spawning Vats (E2M7) Main","pro":False},
|
||||
{"target":"Spawning Vats (E2M7) Red Exit","pro":False}]},
|
||||
"connections":["Spawning Vats (E2M7) Main"]},
|
||||
{"name":"Spawning Vats (E2M7) Entrance Secret",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]},
|
||||
{"name":"Spawning Vats (E2M7) Red Exit",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
"connections":["Spawning Vats (E2M7) Main"]},
|
||||
|
||||
# Tower of Babel (E2M8)
|
||||
{"name":"Tower of Babel (E2M8) Main",
|
||||
@@ -323,134 +295,134 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Fortress of Mystery (E2M9) Blue","pro":False},
|
||||
{"target":"Fortress of Mystery (E2M9) Red","pro":False},
|
||||
{"target":"Fortress of Mystery (E2M9) Yellow","pro":False}]},
|
||||
"Fortress of Mystery (E2M9) Blue",
|
||||
"Fortress of Mystery (E2M9) Red",
|
||||
"Fortress of Mystery (E2M9) Yellow"]},
|
||||
{"name":"Fortress of Mystery (E2M9) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]},
|
||||
"connections":["Fortress of Mystery (E2M9) Main"]},
|
||||
{"name":"Fortress of Mystery (E2M9) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]},
|
||||
"connections":["Fortress of Mystery (E2M9) Main"]},
|
||||
{"name":"Fortress of Mystery (E2M9) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]},
|
||||
"connections":["Fortress of Mystery (E2M9) Main"]},
|
||||
|
||||
# Hell Keep (E3M1)
|
||||
{"name":"Hell Keep (E3M1) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Hell Keep (E3M1) Narrow","pro":False}]},
|
||||
"connections":["Hell Keep (E3M1) Narrow"]},
|
||||
{"name":"Hell Keep (E3M1) Narrow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Hell Keep (E3M1) Main","pro":False}]},
|
||||
"connections":["Hell Keep (E3M1) Main"]},
|
||||
|
||||
# Slough of Despair (E3M2)
|
||||
{"name":"Slough of Despair (E3M2) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Slough of Despair (E3M2) Blue","pro":False}]},
|
||||
"connections":["Slough of Despair (E3M2) Blue"]},
|
||||
{"name":"Slough of Despair (E3M2) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Slough of Despair (E3M2) Main","pro":False}]},
|
||||
"connections":["Slough of Despair (E3M2) Main"]},
|
||||
|
||||
# Pandemonium (E3M3)
|
||||
{"name":"Pandemonium (E3M3) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Pandemonium (E3M3) Blue","pro":False}]},
|
||||
"connections":["Pandemonium (E3M3) Blue"]},
|
||||
{"name":"Pandemonium (E3M3) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Pandemonium (E3M3) Main","pro":False}]},
|
||||
"connections":["Pandemonium (E3M3) Main"]},
|
||||
|
||||
# House of Pain (E3M4)
|
||||
{"name":"House of Pain (E3M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]},
|
||||
"connections":["House of Pain (E3M4) Blue"]},
|
||||
{"name":"House of Pain (E3M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"House of Pain (E3M4) Main","pro":False},
|
||||
{"target":"House of Pain (E3M4) Yellow","pro":False},
|
||||
{"target":"House of Pain (E3M4) Red","pro":False}]},
|
||||
"House of Pain (E3M4) Main",
|
||||
"House of Pain (E3M4) Yellow",
|
||||
"House of Pain (E3M4) Red"]},
|
||||
{"name":"House of Pain (E3M4) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]},
|
||||
"connections":["House of Pain (E3M4) Blue"]},
|
||||
{"name":"House of Pain (E3M4) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]},
|
||||
"connections":["House of Pain (E3M4) Blue"]},
|
||||
|
||||
# Unholy Cathedral (E3M5)
|
||||
{"name":"Unholy Cathedral (E3M5) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Unholy Cathedral (E3M5) Yellow","pro":False},
|
||||
{"target":"Unholy Cathedral (E3M5) Blue","pro":False}]},
|
||||
"Unholy Cathedral (E3M5) Yellow",
|
||||
"Unholy Cathedral (E3M5) Blue"]},
|
||||
{"name":"Unholy Cathedral (E3M5) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
|
||||
"connections":["Unholy Cathedral (E3M5) Main"]},
|
||||
{"name":"Unholy Cathedral (E3M5) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
|
||||
"connections":["Unholy Cathedral (E3M5) Main"]},
|
||||
|
||||
# Mt. Erebus (E3M6)
|
||||
{"name":"Mt. Erebus (E3M6) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Mt. Erebus (E3M6) Blue","pro":False}]},
|
||||
"connections":["Mt. Erebus (E3M6) Blue"]},
|
||||
{"name":"Mt. Erebus (E3M6) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Mt. Erebus (E3M6) Main","pro":False}]},
|
||||
"connections":["Mt. Erebus (E3M6) Main"]},
|
||||
|
||||
# Limbo (E3M7)
|
||||
{"name":"Limbo (E3M7) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Limbo (E3M7) Red","pro":False},
|
||||
{"target":"Limbo (E3M7) Blue","pro":False},
|
||||
{"target":"Limbo (E3M7) Pink","pro":False}]},
|
||||
"Limbo (E3M7) Red",
|
||||
"Limbo (E3M7) Blue",
|
||||
"Limbo (E3M7) Pink"]},
|
||||
{"name":"Limbo (E3M7) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Limbo (E3M7) Main","pro":False}]},
|
||||
"connections":["Limbo (E3M7) Main"]},
|
||||
{"name":"Limbo (E3M7) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Limbo (E3M7) Main","pro":False},
|
||||
{"target":"Limbo (E3M7) Yellow","pro":False},
|
||||
{"target":"Limbo (E3M7) Green","pro":False}]},
|
||||
"Limbo (E3M7) Main",
|
||||
"Limbo (E3M7) Yellow",
|
||||
"Limbo (E3M7) Green"]},
|
||||
{"name":"Limbo (E3M7) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Limbo (E3M7) Red","pro":False}]},
|
||||
"connections":["Limbo (E3M7) Red"]},
|
||||
{"name":"Limbo (E3M7) Pink",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Limbo (E3M7) Green","pro":False},
|
||||
{"target":"Limbo (E3M7) Main","pro":False}]},
|
||||
"Limbo (E3M7) Green",
|
||||
"Limbo (E3M7) Main"]},
|
||||
{"name":"Limbo (E3M7) Green",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Limbo (E3M7) Pink","pro":False},
|
||||
{"target":"Limbo (E3M7) Red","pro":False}]},
|
||||
"Limbo (E3M7) Pink",
|
||||
"Limbo (E3M7) Red"]},
|
||||
|
||||
# Dis (E3M8)
|
||||
{"name":"Dis (E3M8) Main",
|
||||
@@ -463,8 +435,8 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Warrens (E3M9) Blue","pro":False},
|
||||
{"target":"Warrens (E3M9) Blue trigger","pro":False}]},
|
||||
"Warrens (E3M9) Blue",
|
||||
"Warrens (E3M9) Blue trigger"]},
|
||||
{"name":"Warrens (E3M9) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
@@ -473,8 +445,8 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Warrens (E3M9) Main","pro":False},
|
||||
{"target":"Warrens (E3M9) Red","pro":False}]},
|
||||
"Warrens (E3M9) Main",
|
||||
"Warrens (E3M9) Red"]},
|
||||
{"name":"Warrens (E3M9) Blue trigger",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
@@ -485,36 +457,36 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Hell Beneath (E4M1) Red","pro":False},
|
||||
{"target":"Hell Beneath (E4M1) Blue","pro":False}]},
|
||||
"Hell Beneath (E4M1) Red",
|
||||
"Hell Beneath (E4M1) Blue"]},
|
||||
{"name":"Hell Beneath (E4M1) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]},
|
||||
"connections":["Hell Beneath (E4M1) Main"]},
|
||||
{"name":"Hell Beneath (E4M1) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]},
|
||||
"connections":["Hell Beneath (E4M1) Main"]},
|
||||
|
||||
# Perfect Hatred (E4M2)
|
||||
{"name":"Perfect Hatred (E4M2) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Perfect Hatred (E4M2) Blue","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Yellow","pro":False}]},
|
||||
"Perfect Hatred (E4M2) Blue",
|
||||
"Perfect Hatred (E4M2) Yellow"]},
|
||||
{"name":"Perfect Hatred (E4M2) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Perfect Hatred (E4M2) Main","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
|
||||
"Perfect Hatred (E4M2) Main",
|
||||
"Perfect Hatred (E4M2) Cave"]},
|
||||
{"name":"Perfect Hatred (E4M2) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Perfect Hatred (E4M2) Main","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
|
||||
"Perfect Hatred (E4M2) Main",
|
||||
"Perfect Hatred (E4M2) Cave"]},
|
||||
{"name":"Perfect Hatred (E4M2) Cave",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
@@ -524,135 +496,132 @@ regions:List[RegionDict] = [
|
||||
{"name":"Sever the Wicked (E4M3) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]},
|
||||
"connections":["Sever the Wicked (E4M3) Red"]},
|
||||
{"name":"Sever the Wicked (E4M3) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Sever the Wicked (E4M3) Blue","pro":False},
|
||||
{"target":"Sever the Wicked (E4M3) Main","pro":False}]},
|
||||
"Sever the Wicked (E4M3) Blue",
|
||||
"Sever the Wicked (E4M3) Main"]},
|
||||
{"name":"Sever the Wicked (E4M3) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]},
|
||||
"connections":["Sever the Wicked (E4M3) Red"]},
|
||||
|
||||
# Unruly Evil (E4M4)
|
||||
{"name":"Unruly Evil (E4M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Unruly Evil (E4M4) Red","pro":False}]},
|
||||
"connections":["Unruly Evil (E4M4) Red"]},
|
||||
{"name":"Unruly Evil (E4M4) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Unruly Evil (E4M4) Main","pro":False}]},
|
||||
"connections":["Unruly Evil (E4M4) Main"]},
|
||||
|
||||
# They Will Repent (E4M5)
|
||||
{"name":"They Will Repent (E4M5) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]},
|
||||
"connections":["They Will Repent (E4M5) Red"]},
|
||||
{"name":"They Will Repent (E4M5) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]},
|
||||
"connections":["They Will Repent (E4M5) Red"]},
|
||||
{"name":"They Will Repent (E4M5) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]},
|
||||
"connections":["They Will Repent (E4M5) Red"]},
|
||||
{"name":"They Will Repent (E4M5) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"They Will Repent (E4M5) Main","pro":False},
|
||||
{"target":"They Will Repent (E4M5) Yellow","pro":False},
|
||||
{"target":"They Will Repent (E4M5) Blue","pro":False}]},
|
||||
"They Will Repent (E4M5) Main",
|
||||
"They Will Repent (E4M5) Yellow",
|
||||
"They Will Repent (E4M5) Blue"]},
|
||||
|
||||
# Against Thee Wickedly (E4M6)
|
||||
{"name":"Against Thee Wickedly (E4M6) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Against Thee Wickedly (E4M6) Blue","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Pink","pro":True}]},
|
||||
"connections":["Against Thee Wickedly (E4M6) Blue"]},
|
||||
{"name":"Against Thee Wickedly (E4M6) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Against Thee Wickedly (E4M6) Blue","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Pink","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Main","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Magenta","pro":True}]},
|
||||
"Against Thee Wickedly (E4M6) Blue",
|
||||
"Against Thee Wickedly (E4M6) Pink",
|
||||
"Against Thee Wickedly (E4M6) Main"]},
|
||||
{"name":"Against Thee Wickedly (E4M6) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Against Thee Wickedly (E4M6) Main","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Yellow","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Red","pro":False}]},
|
||||
"Against Thee Wickedly (E4M6) Main",
|
||||
"Against Thee Wickedly (E4M6) Yellow",
|
||||
"Against Thee Wickedly (E4M6) Red"]},
|
||||
{"name":"Against Thee Wickedly (E4M6) Magenta",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]},
|
||||
"connections":["Against Thee Wickedly (E4M6) Main"]},
|
||||
{"name":"Against Thee Wickedly (E4M6) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Against Thee Wickedly (E4M6) Blue","pro":False},
|
||||
{"target":"Against Thee Wickedly (E4M6) Magenta","pro":False}]},
|
||||
"Against Thee Wickedly (E4M6) Blue",
|
||||
"Against Thee Wickedly (E4M6) Magenta"]},
|
||||
{"name":"Against Thee Wickedly (E4M6) Pink",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]},
|
||||
"connections":["Against Thee Wickedly (E4M6) Main"]},
|
||||
|
||||
# And Hell Followed (E4M7)
|
||||
{"name":"And Hell Followed (E4M7) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"And Hell Followed (E4M7) Blue","pro":False},
|
||||
{"target":"And Hell Followed (E4M7) Red","pro":False},
|
||||
{"target":"And Hell Followed (E4M7) Yellow","pro":False}]},
|
||||
"And Hell Followed (E4M7) Blue",
|
||||
"And Hell Followed (E4M7) Red",
|
||||
"And Hell Followed (E4M7) Yellow"]},
|
||||
{"name":"And Hell Followed (E4M7) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]},
|
||||
"connections":["And Hell Followed (E4M7) Main"]},
|
||||
{"name":"And Hell Followed (E4M7) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]},
|
||||
"connections":["And Hell Followed (E4M7) Main"]},
|
||||
{"name":"And Hell Followed (E4M7) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]},
|
||||
"connections":["And Hell Followed (E4M7) Main"]},
|
||||
|
||||
# Unto the Cruel (E4M8)
|
||||
{"name":"Unto the Cruel (E4M8) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Unto the Cruel (E4M8) Red","pro":False},
|
||||
{"target":"Unto the Cruel (E4M8) Yellow","pro":False},
|
||||
{"target":"Unto the Cruel (E4M8) Orange","pro":False}]},
|
||||
"Unto the Cruel (E4M8) Red",
|
||||
"Unto the Cruel (E4M8) Yellow",
|
||||
"Unto the Cruel (E4M8) Orange"]},
|
||||
{"name":"Unto the Cruel (E4M8) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]},
|
||||
"connections":["Unto the Cruel (E4M8) Main"]},
|
||||
{"name":"Unto the Cruel (E4M8) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]},
|
||||
"connections":["Unto the Cruel (E4M8) Main"]},
|
||||
{"name":"Unto the Cruel (E4M8) Orange",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]},
|
||||
"connections":["Unto the Cruel (E4M8) Main"]},
|
||||
|
||||
# Fear (E4M9)
|
||||
{"name":"Fear (E4M9) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Fear (E4M9) Yellow","pro":False}]},
|
||||
"connections":["Fear (E4M9) Yellow"]},
|
||||
{"name":"Fear (E4M9) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Fear (E4M9) Main","pro":False}]},
|
||||
"connections":["Fear (E4M9) Main"]},
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
|
||||
from . import DOOM1993World
|
||||
|
||||
|
||||
def set_episode1_rules(player, world, pro):
|
||||
def set_episode1_rules(player, world):
|
||||
# Hangar (E1M1)
|
||||
set_rule(world.get_entrance("Hub -> Hangar (E1M1) Main", player), lambda state:
|
||||
state.has("Hangar (E1M1)", player, 1))
|
||||
@@ -130,7 +130,7 @@ def set_episode1_rules(player, world, pro):
|
||||
state.has("Military Base (E1M9) - Yellow keycard", player, 1))
|
||||
|
||||
|
||||
def set_episode2_rules(player, world, pro):
|
||||
def set_episode2_rules(player, world):
|
||||
# Deimos Anomaly (E2M1)
|
||||
set_rule(world.get_entrance("Hub -> Deimos Anomaly (E2M1) Main", player), lambda state:
|
||||
state.has("Deimos Anomaly (E2M1)", player, 1))
|
||||
@@ -226,9 +226,6 @@ def set_episode2_rules(player, world, pro):
|
||||
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Yellow", player), lambda state:
|
||||
state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1))
|
||||
if pro:
|
||||
set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Red Exit", player), lambda state:
|
||||
state.has("Rocket launcher", player, 1))
|
||||
set_rule(world.get_entrance("Spawning Vats (E2M7) Yellow -> Spawning Vats (E2M7) Main", player), lambda state:
|
||||
state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("Spawning Vats (E2M7) Red -> Spawning Vats (E2M7) Main", player), lambda state:
|
||||
@@ -263,7 +260,7 @@ def set_episode2_rules(player, world, pro):
|
||||
state.has("Fortress of Mystery (E2M9) - Yellow skull key", player, 1))
|
||||
|
||||
|
||||
def set_episode3_rules(player, world, pro):
|
||||
def set_episode3_rules(player, world):
|
||||
# Hell Keep (E3M1)
|
||||
set_rule(world.get_entrance("Hub -> Hell Keep (E3M1) Main", player), lambda state:
|
||||
state.has("Hell Keep (E3M1)", player, 1))
|
||||
@@ -388,7 +385,7 @@ def set_episode3_rules(player, world, pro):
|
||||
state.has("Warrens (E3M9) - Red skull key", player, 1))
|
||||
|
||||
|
||||
def set_episode4_rules(player, world, pro):
|
||||
def set_episode4_rules(player, world):
|
||||
# Hell Beneath (E4M1)
|
||||
set_rule(world.get_entrance("Hub -> Hell Beneath (E4M1) Main", player), lambda state:
|
||||
state.has("Hell Beneath (E4M1)", player, 1))
|
||||
@@ -523,15 +520,15 @@ def set_episode4_rules(player, world, pro):
|
||||
state.has("Fear (E4M9) - Yellow skull key", player, 1))
|
||||
|
||||
|
||||
def set_rules(doom_1993_world: "DOOM1993World", included_episodes, pro):
|
||||
def set_rules(doom_1993_world: "DOOM1993World", included_episodes):
|
||||
player = doom_1993_world.player
|
||||
world = doom_1993_world.multiworld
|
||||
|
||||
if included_episodes[0]:
|
||||
set_episode1_rules(player, world, pro)
|
||||
set_episode1_rules(player, world)
|
||||
if included_episodes[1]:
|
||||
set_episode2_rules(player, world, pro)
|
||||
set_episode2_rules(player, world)
|
||||
if included_episodes[2]:
|
||||
set_episode3_rules(player, world, pro)
|
||||
set_episode3_rules(player, world)
|
||||
if included_episodes[3]:
|
||||
set_episode4_rules(player, world, pro)
|
||||
set_episode4_rules(player, world)
|
||||
|
||||
@@ -56,13 +56,6 @@ class DOOM1993World(World):
|
||||
"Hell Beneath (E4M1)"
|
||||
]
|
||||
|
||||
boss_level_for_espidoes: List[str] = [
|
||||
"Phobos Anomaly (E1M8)",
|
||||
"Tower of Babel (E2M8)",
|
||||
"Dis (E3M8)",
|
||||
"Unto the Cruel (E4M8)"
|
||||
]
|
||||
|
||||
# Item ratio that scales depending on episode count. These are the ratio for 3 episode.
|
||||
items_ratio: Dict[str, float] = {
|
||||
"Armor": 41,
|
||||
@@ -97,8 +90,6 @@ class DOOM1993World(World):
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
def create_regions(self):
|
||||
pro = getattr(self.multiworld, "pro")[self.player].value
|
||||
|
||||
# Main regions
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
hub_region = Region("Hub", self.player, self.multiworld)
|
||||
@@ -125,11 +116,8 @@ class DOOM1993World(World):
|
||||
|
||||
self.multiworld.regions.append(region)
|
||||
|
||||
for connection_dict in region_dict["connections"]:
|
||||
# Check if it's a pro-only connection
|
||||
if connection_dict["pro"] and not pro:
|
||||
continue
|
||||
connections.append((region, connection_dict["target"]))
|
||||
for connection in region_dict["connections"]:
|
||||
connections.append((region, connection))
|
||||
|
||||
# Connect main regions to Hub
|
||||
hub_region.add_exits(main_regions)
|
||||
@@ -147,11 +135,7 @@ class DOOM1993World(World):
|
||||
self.location_count = len(self.multiworld.get_locations(self.player))
|
||||
|
||||
def completion_rule(self, state: CollectionState):
|
||||
goal_levels = Maps.map_names
|
||||
if getattr(self.multiworld, "goal")[self.player].value:
|
||||
goal_levels = self.boss_level_for_espidoes
|
||||
|
||||
for map_name in goal_levels:
|
||||
for map_name in Maps.map_names:
|
||||
if map_name + " - Exit" not in self.location_name_to_id:
|
||||
continue
|
||||
|
||||
@@ -167,15 +151,12 @@ class DOOM1993World(World):
|
||||
return True
|
||||
|
||||
def set_rules(self):
|
||||
pro = getattr(self.multiworld, "pro")[self.player].value
|
||||
allow_death_logic = getattr(self.multiworld, "allow_death_logic")[self.player].value
|
||||
|
||||
Rules.set_rules(self, self.included_episodes, pro)
|
||||
Rules.set_rules(self, self.included_episodes)
|
||||
self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state)
|
||||
|
||||
# Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed
|
||||
# platform) Unless the user allows for it.
|
||||
if not allow_death_logic:
|
||||
if not getattr(self.multiworld, "allow_death_logic")[self.player].value:
|
||||
for death_logic_location in Locations.death_logic_locations:
|
||||
self.multiworld.exclude_locations[self.player].value.add(death_logic_location)
|
||||
|
||||
@@ -184,6 +165,7 @@ class DOOM1993World(World):
|
||||
return DOOM1993Item(name, Items.item_table[item_id]["classification"], item_id, self.player)
|
||||
|
||||
def create_items(self):
|
||||
is_only_first_episode: bool = self.get_episode_count() == 1 and self.included_episodes[0]
|
||||
itempool: List[DOOM1993Item] = []
|
||||
start_with_computer_area_maps: bool = getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value
|
||||
|
||||
@@ -198,6 +180,9 @@ class DOOM1993World(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
if item["name"] in {"BFG9000", "Plasma Gun"} and is_only_first_episode:
|
||||
continue # Don't include those guns if only first episode
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
@@ -227,10 +212,8 @@ class DOOM1993World(World):
|
||||
# Give Computer area maps if option selected
|
||||
if getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value:
|
||||
for item_id, item_dict in Items.item_table.items():
|
||||
item_episode = item_dict["episode"]
|
||||
if item_episode > 0:
|
||||
if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]:
|
||||
self.multiworld.push_precollected(self.create_item(item_dict["name"]))
|
||||
if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP:
|
||||
self.multiworld.push_precollected(self.create_item(item_dict["name"]))
|
||||
|
||||
# Fill the rest starting with powerups, then fillers
|
||||
self.create_ratioed_items("Armor", itempool)
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
## Optional Software
|
||||
|
||||
- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [PopTracker](https://github.com/black-sliver/PopTracker/)
|
||||
- [OZone's APDoom tracker pack](https://github.com/Ozone31/doom-ap-tracker/releases)
|
||||
|
||||
## Installing AP Doom
|
||||
1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it.
|
||||
@@ -19,11 +17,10 @@
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Launch apdoom-launcher.exe
|
||||
2. Select `Ultimate DOOM` from the drop-down
|
||||
3. Enter the Archipelago server address, slot name, and password (if you have one)
|
||||
4. Press "Launch DOOM"
|
||||
5. Enjoy!
|
||||
1. Launch APDoomLauncher.exe
|
||||
2. Enter the Archipelago server address, slot name, and password (if you have one)
|
||||
3. Press "Launch DOOM"
|
||||
4. Enjoy!
|
||||
|
||||
To continue a game, follow the same connection steps.
|
||||
Connecting with a different seed won't erase your progress in other seeds.
|
||||
@@ -34,23 +31,8 @@ We recommend having Archipelago's Text Client open on the side to keep track of
|
||||
APDOOM has in-game messages,
|
||||
but they disappear quickly and there's no reasonable way to check your message history in-game.
|
||||
|
||||
### Hinting
|
||||
|
||||
To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM can be difficult because names are rather long and contain special characters. For example:
|
||||
```
|
||||
!hint Toxin Refinery (E1M3) - Computer area map
|
||||
```
|
||||
The game has a hint helper implemented, where you can simply type this:
|
||||
```
|
||||
!hint e1m3 map
|
||||
```
|
||||
For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`.
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
APDOOM has a functional map tracker integrated into the level select screen.
|
||||
It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed,
|
||||
and how many of the checks you have completed in each level.
|
||||
|
||||
For better tracking, try OZone's poptracker package: https://github.com/Ozone31/doom-ap-tracker/releases .
|
||||
Requires [PopTracker](https://github.com/black-sliver/PopTracker/).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
# This file is auto generated. More info: https://github.com/Daivuk/apdoom
|
||||
|
||||
from typing import List
|
||||
|
||||
|
||||
map_names: List[str] = [
|
||||
'Entryway (MAP01)',
|
||||
'Underhalls (MAP02)',
|
||||
'The Gantlet (MAP03)',
|
||||
'The Focus (MAP04)',
|
||||
'The Waste Tunnels (MAP05)',
|
||||
'The Crusher (MAP06)',
|
||||
'Dead Simple (MAP07)',
|
||||
'Tricks and Traps (MAP08)',
|
||||
'The Pit (MAP09)',
|
||||
'Refueling Base (MAP10)',
|
||||
'Circle of Death (MAP11)',
|
||||
'The Factory (MAP12)',
|
||||
'Downtown (MAP13)',
|
||||
'The Inmost Dens (MAP14)',
|
||||
'Industrial Zone (MAP15)',
|
||||
'Suburbs (MAP16)',
|
||||
'Tenements (MAP17)',
|
||||
'The Courtyard (MAP18)',
|
||||
'The Citadel (MAP19)',
|
||||
'Gotcha! (MAP20)',
|
||||
'Nirvana (MAP21)',
|
||||
'The Catacombs (MAP22)',
|
||||
'Barrels o Fun (MAP23)',
|
||||
'The Chasm (MAP24)',
|
||||
'Bloodfalls (MAP25)',
|
||||
'The Abandoned Mines (MAP26)',
|
||||
'Monster Condo (MAP27)',
|
||||
'The Spirit World (MAP28)',
|
||||
'The Living End (MAP29)',
|
||||
'Icon of Sin (MAP30)',
|
||||
'Wolfenstein2 (MAP31)',
|
||||
'Grosse2 (MAP32)',
|
||||
]
|
||||
@@ -1,150 +0,0 @@
|
||||
import typing
|
||||
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""
|
||||
Choose the difficulty option. Those match DOOM's difficulty options.
|
||||
baby (I'm too young to die.) double ammos, half damage, less monsters or strength.
|
||||
easy (Hey, not too rough.) less monsters or strength.
|
||||
medium (Hurt me plenty.) Default.
|
||||
hard (Ultra-Violence.) More monsters or strength.
|
||||
nightmare (Nightmare!) Monsters attack more rapidly and respawn.
|
||||
"""
|
||||
display_name = "Difficulty"
|
||||
option_baby = 0
|
||||
option_easy = 1
|
||||
option_medium = 2
|
||||
option_hard = 3
|
||||
option_nightmare = 4
|
||||
default = 2
|
||||
|
||||
|
||||
class RandomMonsters(Choice):
|
||||
"""
|
||||
Choose how monsters are randomized.
|
||||
vanilla: No randomization
|
||||
shuffle: Monsters are shuffled within the level
|
||||
random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big)
|
||||
random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game.
|
||||
"""
|
||||
display_name = "Random Monsters"
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_random_balanced = 2
|
||||
option_random_chaotic = 3
|
||||
default = 2
|
||||
|
||||
|
||||
class RandomPickups(Choice):
|
||||
"""
|
||||
Choose how pickups are randomized.
|
||||
vanilla: No randomization
|
||||
shuffle: Pickups are shuffled within the level
|
||||
random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big)
|
||||
"""
|
||||
display_name = "Random Pickups"
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_random_balanced = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class RandomMusic(Choice):
|
||||
"""
|
||||
Level musics will be randomized.
|
||||
vanilla: No randomization
|
||||
shuffle_selected: Selected episodes' levels will be shuffled
|
||||
shuffle_game: All the music will be shuffled
|
||||
"""
|
||||
display_name = "Random Music"
|
||||
option_vanilla = 0
|
||||
option_shuffle_selected = 1
|
||||
option_shuffle_game = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class FlipLevels(Choice):
|
||||
"""
|
||||
Flip levels on one axis.
|
||||
vanilla: No flipping
|
||||
flipped: All levels are flipped
|
||||
random: Random levels are flipped
|
||||
"""
|
||||
display_name = "Flip Levels"
|
||||
option_vanilla = 0
|
||||
option_flipped = 1
|
||||
option_randomly_flipped = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class AllowDeathLogic(Toggle):
|
||||
"""Some locations require a timed puzzle that can only be tried once.
|
||||
After which, if the player failed to get it, the location cannot be checked anymore.
|
||||
By default, no progression items are placed here. There is a way, hovewer, to still get them:
|
||||
Get killed in the current map. The map will reset, you can now attempt the puzzle again."""
|
||||
display_name = "Allow Death Logic"
|
||||
|
||||
|
||||
class Pro(Toggle):
|
||||
"""Include difficult tricks into rules. Mostly employed by speed runners.
|
||||
i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc."""
|
||||
display_name = "Pro Doom"
|
||||
|
||||
|
||||
class StartWithComputerAreaMaps(Toggle):
|
||||
"""Give the player all Computer Area Map items from the start."""
|
||||
display_name = "Start With Computer Area Maps"
|
||||
|
||||
|
||||
class ResetLevelOnDeath(DefaultOnToggle):
|
||||
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
|
||||
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
|
||||
display_message="Reset level on death"
|
||||
|
||||
|
||||
class Episode1(DefaultOnToggle):
|
||||
"""Subterranean and Outpost.
|
||||
If none of the episodes are chosen, Episode 1 will be chosen by default."""
|
||||
display_name = "Episode 1"
|
||||
|
||||
|
||||
class Episode2(DefaultOnToggle):
|
||||
"""City.
|
||||
If none of the episodes are chosen, Episode 1 will be chosen by default."""
|
||||
display_name = "Episode 2"
|
||||
|
||||
|
||||
class Episode3(DefaultOnToggle):
|
||||
"""Hell.
|
||||
If none of the episodes are chosen, Episode 1 will be chosen by default."""
|
||||
display_name = "Episode 3"
|
||||
|
||||
|
||||
class SecretLevels(Toggle):
|
||||
"""Secret levels.
|
||||
This is too short to be an episode. It's additive.
|
||||
Another episode will have to be selected along with this one.
|
||||
Otherwise episode 1 will be added."""
|
||||
display_name = "Secret Levels"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DOOM2Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
difficulty: Difficulty
|
||||
random_monsters: RandomMonsters
|
||||
random_pickups: RandomPickups
|
||||
random_music: RandomMusic
|
||||
flip_levels: FlipLevels
|
||||
allow_death_logic: AllowDeathLogic
|
||||
pro: Pro
|
||||
start_with_computer_area_maps: StartWithComputerAreaMaps
|
||||
death_link: DeathLink
|
||||
reset_level_on_death: ResetLevelOnDeath
|
||||
episode1: Episode1
|
||||
episode2: Episode2
|
||||
episode3: Episode3
|
||||
episode4: SecretLevels
|
||||
@@ -1,502 +0,0 @@
|
||||
# This file is auto generated. More info: https://github.com/Daivuk/apdoom
|
||||
|
||||
from typing import List
|
||||
from BaseClasses import TypedDict
|
||||
|
||||
class ConnectionDict(TypedDict, total=False):
|
||||
target: str
|
||||
pro: bool
|
||||
|
||||
class RegionDict(TypedDict, total=False):
|
||||
name: str
|
||||
connects_to_hub: bool
|
||||
episode: int
|
||||
connections: List[ConnectionDict]
|
||||
|
||||
|
||||
regions:List[RegionDict] = [
|
||||
# Entryway (MAP01)
|
||||
{"name":"Entryway (MAP01) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[]},
|
||||
|
||||
# Underhalls (MAP02)
|
||||
{"name":"Underhalls (MAP02) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Underhalls (MAP02) Red","pro":False}]},
|
||||
{"name":"Underhalls (MAP02) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Underhalls (MAP02) Red","pro":False}]},
|
||||
{"name":"Underhalls (MAP02) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Underhalls (MAP02) Blue","pro":False},
|
||||
{"target":"Underhalls (MAP02) Main","pro":False}]},
|
||||
|
||||
# The Gantlet (MAP03)
|
||||
{"name":"The Gantlet (MAP03) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Gantlet (MAP03) Blue","pro":False},
|
||||
{"target":"The Gantlet (MAP03) Blue Pro Jump","pro":True}]},
|
||||
{"name":"The Gantlet (MAP03) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Gantlet (MAP03) Main","pro":False},
|
||||
{"target":"The Gantlet (MAP03) Red","pro":False},
|
||||
{"target":"The Gantlet (MAP03) Blue Pro Jump","pro":False}]},
|
||||
{"name":"The Gantlet (MAP03) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[]},
|
||||
{"name":"The Gantlet (MAP03) Blue Pro Jump",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Gantlet (MAP03) Blue","pro":False}]},
|
||||
|
||||
# The Focus (MAP04)
|
||||
{"name":"The Focus (MAP04) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Focus (MAP04) Red","pro":False},
|
||||
{"target":"The Focus (MAP04) Blue","pro":False}]},
|
||||
{"name":"The Focus (MAP04) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Focus (MAP04) Main","pro":False}]},
|
||||
{"name":"The Focus (MAP04) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Focus (MAP04) Red","pro":False}]},
|
||||
{"name":"The Focus (MAP04) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Focus (MAP04) Yellow","pro":False},
|
||||
{"target":"The Focus (MAP04) Main","pro":False}]},
|
||||
|
||||
# The Waste Tunnels (MAP05)
|
||||
{"name":"The Waste Tunnels (MAP05) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Waste Tunnels (MAP05) Red","pro":False},
|
||||
{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Waste Tunnels (MAP05) Yellow","pro":False},
|
||||
{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
|
||||
|
||||
# The Crusher (MAP06)
|
||||
{"name":"The Crusher (MAP06) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Crusher (MAP06) Blue","pro":False}]},
|
||||
{"name":"The Crusher (MAP06) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Crusher (MAP06) Red","pro":False},
|
||||
{"target":"The Crusher (MAP06) Main","pro":False}]},
|
||||
{"name":"The Crusher (MAP06) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Crusher (MAP06) Red","pro":False}]},
|
||||
{"name":"The Crusher (MAP06) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Crusher (MAP06) Yellow","pro":False},
|
||||
{"target":"The Crusher (MAP06) Blue","pro":False},
|
||||
{"target":"The Crusher (MAP06) Main","pro":False}]},
|
||||
|
||||
# Dead Simple (MAP07)
|
||||
{"name":"Dead Simple (MAP07) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[]},
|
||||
|
||||
# Tricks and Traps (MAP08)
|
||||
{"name":"Tricks and Traps (MAP08) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Tricks and Traps (MAP08) Red","pro":False},
|
||||
{"target":"Tricks and Traps (MAP08) Yellow","pro":False}]},
|
||||
{"name":"Tricks and Traps (MAP08) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]},
|
||||
{"name":"Tricks and Traps (MAP08) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]},
|
||||
|
||||
# The Pit (MAP09)
|
||||
{"name":"The Pit (MAP09) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Pit (MAP09) Yellow","pro":False},
|
||||
{"target":"The Pit (MAP09) Blue","pro":False}]},
|
||||
{"name":"The Pit (MAP09) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[]},
|
||||
{"name":"The Pit (MAP09) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
|
||||
|
||||
# Refueling Base (MAP10)
|
||||
{"name":"Refueling Base (MAP10) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
|
||||
{"name":"Refueling Base (MAP10) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Refueling Base (MAP10) Main","pro":False},
|
||||
{"target":"Refueling Base (MAP10) Yellow Blue","pro":False}]},
|
||||
{"name":"Refueling Base (MAP10) Yellow Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
|
||||
|
||||
# Circle of Death (MAP11)
|
||||
{"name":"Circle of Death (MAP11) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Circle of Death (MAP11) Blue","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Red","pro":False}]},
|
||||
{"name":"Circle of Death (MAP11) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
|
||||
{"name":"Circle of Death (MAP11) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
|
||||
|
||||
# The Factory (MAP12)
|
||||
{"name":"The Factory (MAP12) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Factory (MAP12) Yellow","pro":False},
|
||||
{"target":"The Factory (MAP12) Blue","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
|
||||
# Downtown (MAP13)
|
||||
{"name":"Downtown (MAP13) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Downtown (MAP13) Yellow","pro":False},
|
||||
{"target":"Downtown (MAP13) Red","pro":False},
|
||||
{"target":"Downtown (MAP13) Blue","pro":False}]},
|
||||
{"name":"Downtown (MAP13) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Downtown (MAP13) Main","pro":False}]},
|
||||
{"name":"Downtown (MAP13) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Downtown (MAP13) Main","pro":False}]},
|
||||
{"name":"Downtown (MAP13) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Downtown (MAP13) Main","pro":False}]},
|
||||
|
||||
# The Inmost Dens (MAP14)
|
||||
{"name":"The Inmost Dens (MAP14) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Inmost Dens (MAP14) Red","pro":False}]},
|
||||
{"name":"The Inmost Dens (MAP14) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Inmost Dens (MAP14) Main","pro":False},
|
||||
{"target":"The Inmost Dens (MAP14) Red East","pro":False}]},
|
||||
{"name":"The Inmost Dens (MAP14) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Inmost Dens (MAP14) Main","pro":False},
|
||||
{"target":"The Inmost Dens (MAP14) Red South","pro":False},
|
||||
{"target":"The Inmost Dens (MAP14) Red East","pro":False}]},
|
||||
{"name":"The Inmost Dens (MAP14) Red East",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Inmost Dens (MAP14) Blue","pro":False},
|
||||
{"target":"The Inmost Dens (MAP14) Main","pro":False}]},
|
||||
{"name":"The Inmost Dens (MAP14) Red South",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Inmost Dens (MAP14) Main","pro":False}]},
|
||||
|
||||
# Industrial Zone (MAP15)
|
||||
{"name":"Industrial Zone (MAP15) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Industrial Zone (MAP15) Yellow East","pro":False},
|
||||
{"target":"Industrial Zone (MAP15) Yellow West","pro":False}]},
|
||||
{"name":"Industrial Zone (MAP15) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Industrial Zone (MAP15) Yellow East","pro":False}]},
|
||||
{"name":"Industrial Zone (MAP15) Yellow East",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Industrial Zone (MAP15) Blue","pro":False},
|
||||
{"target":"Industrial Zone (MAP15) Main","pro":False}]},
|
||||
{"name":"Industrial Zone (MAP15) Yellow West",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Industrial Zone (MAP15) Main","pro":False}]},
|
||||
|
||||
# Suburbs (MAP16)
|
||||
{"name":"Suburbs (MAP16) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Suburbs (MAP16) Red","pro":False},
|
||||
{"target":"Suburbs (MAP16) Blue","pro":False}]},
|
||||
{"name":"Suburbs (MAP16) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
|
||||
{"name":"Suburbs (MAP16) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
|
||||
|
||||
# Tenements (MAP17)
|
||||
{"name":"Tenements (MAP17) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Tenements (MAP17) Red","pro":False}]},
|
||||
{"name":"Tenements (MAP17) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Tenements (MAP17) Red","pro":False}]},
|
||||
{"name":"Tenements (MAP17) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Tenements (MAP17) Red","pro":False},
|
||||
{"target":"Tenements (MAP17) Blue","pro":False}]},
|
||||
{"name":"Tenements (MAP17) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Tenements (MAP17) Yellow","pro":False},
|
||||
{"target":"Tenements (MAP17) Blue","pro":False},
|
||||
{"target":"Tenements (MAP17) Main","pro":False}]},
|
||||
|
||||
# The Courtyard (MAP18)
|
||||
{"name":"The Courtyard (MAP18) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Courtyard (MAP18) Yellow","pro":False},
|
||||
{"target":"The Courtyard (MAP18) Blue","pro":False}]},
|
||||
{"name":"The Courtyard (MAP18) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]},
|
||||
{"name":"The Courtyard (MAP18) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]},
|
||||
|
||||
# The Citadel (MAP19)
|
||||
{"name":"The Citadel (MAP19) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Citadel (MAP19) Red","pro":False}]},
|
||||
{"name":"The Citadel (MAP19) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Citadel (MAP19) Main","pro":False}]},
|
||||
|
||||
# Gotcha! (MAP20)
|
||||
{"name":"Gotcha! (MAP20) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
|
||||
# Nirvana (MAP21)
|
||||
{"name":"Nirvana (MAP21) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Nirvana (MAP21) Main","pro":False},
|
||||
{"target":"Nirvana (MAP21) Magenta","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Magenta",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
|
||||
|
||||
# The Catacombs (MAP22)
|
||||
{"name":"The Catacombs (MAP22) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Catacombs (MAP22) Blue","pro":False},
|
||||
{"target":"The Catacombs (MAP22) Red","pro":False}]},
|
||||
{"name":"The Catacombs (MAP22) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
|
||||
{"name":"The Catacombs (MAP22) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
|
||||
|
||||
# Barrels o Fun (MAP23)
|
||||
{"name":"Barrels o Fun (MAP23) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]},
|
||||
{"name":"Barrels o Fun (MAP23) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]},
|
||||
|
||||
# The Chasm (MAP24)
|
||||
{"name":"The Chasm (MAP24) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Red","pro":False}]},
|
||||
{"name":"The Chasm (MAP24) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Main","pro":False}]},
|
||||
|
||||
# Bloodfalls (MAP25)
|
||||
{"name":"Bloodfalls (MAP25) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]},
|
||||
{"name":"Bloodfalls (MAP25) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
|
||||
|
||||
# The Abandoned Mines (MAP26)
|
||||
{"name":"The Abandoned Mines (MAP26) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Abandoned Mines (MAP26) Yellow","pro":False},
|
||||
{"target":"The Abandoned Mines (MAP26) Red","pro":False},
|
||||
{"target":"The Abandoned Mines (MAP26) Blue","pro":False}]},
|
||||
{"name":"The Abandoned Mines (MAP26) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]},
|
||||
{"name":"The Abandoned Mines (MAP26) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]},
|
||||
{"name":"The Abandoned Mines (MAP26) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]},
|
||||
|
||||
# Monster Condo (MAP27)
|
||||
{"name":"Monster Condo (MAP27) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Monster Condo (MAP27) Yellow","pro":False},
|
||||
{"target":"Monster Condo (MAP27) Red","pro":False},
|
||||
{"target":"Monster Condo (MAP27) Blue","pro":False}]},
|
||||
{"name":"Monster Condo (MAP27) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]},
|
||||
{"name":"Monster Condo (MAP27) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]},
|
||||
{"name":"Monster Condo (MAP27) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]},
|
||||
|
||||
# The Spirit World (MAP28)
|
||||
{"name":"The Spirit World (MAP28) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Spirit World (MAP28) Yellow","pro":False},
|
||||
{"target":"The Spirit World (MAP28) Red","pro":False}]},
|
||||
{"name":"The Spirit World (MAP28) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]},
|
||||
{"name":"The Spirit World (MAP28) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]},
|
||||
|
||||
# The Living End (MAP29)
|
||||
{"name":"The Living End (MAP29) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
|
||||
# Icon of Sin (MAP30)
|
||||
{"name":"Icon of Sin (MAP30) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
|
||||
# Wolfenstein2 (MAP31)
|
||||
{"name":"Wolfenstein2 (MAP31) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[]},
|
||||
|
||||
# Grosse2 (MAP32)
|
||||
{"name":"Grosse2 (MAP32) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[]},
|
||||
]
|
||||
@@ -1,501 +0,0 @@
|
||||
# This file is auto generated. More info: https://github.com/Daivuk/apdoom
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import DOOM2World
|
||||
|
||||
|
||||
def set_episode1_rules(player, world, pro):
|
||||
# Entryway (MAP01)
|
||||
set_rule(world.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state:
|
||||
state.has("Entryway (MAP01)", player, 1))
|
||||
set_rule(world.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state:
|
||||
state.has("Entryway (MAP01)", player, 1))
|
||||
|
||||
# Underhalls (MAP02)
|
||||
set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state:
|
||||
state.has("Underhalls (MAP02)", player, 1))
|
||||
set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state:
|
||||
state.has("Underhalls (MAP02)", player, 1))
|
||||
set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state:
|
||||
state.has("Underhalls (MAP02)", player, 1))
|
||||
set_rule(world.get_entrance("Underhalls (MAP02) Main -> Underhalls (MAP02) Red", player), lambda state:
|
||||
state.has("Underhalls (MAP02) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("Underhalls (MAP02) Blue -> Underhalls (MAP02) Red", player), lambda state:
|
||||
state.has("Underhalls (MAP02) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("Underhalls (MAP02) Red -> Underhalls (MAP02) Blue", player), lambda state:
|
||||
state.has("Underhalls (MAP02) - Blue keycard", player, 1))
|
||||
|
||||
# The Gantlet (MAP03)
|
||||
set_rule(world.get_entrance("Hub -> The Gantlet (MAP03) Main", player), lambda state:
|
||||
(state.has("The Gantlet (MAP03)", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)))
|
||||
set_rule(world.get_entrance("The Gantlet (MAP03) Main -> The Gantlet (MAP03) Blue", player), lambda state:
|
||||
state.has("The Gantlet (MAP03) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Gantlet (MAP03) Blue -> The Gantlet (MAP03) Red", player), lambda state:
|
||||
state.has("The Gantlet (MAP03) - Red keycard", player, 1))
|
||||
|
||||
# The Focus (MAP04)
|
||||
set_rule(world.get_entrance("Hub -> The Focus (MAP04) Main", player), lambda state:
|
||||
(state.has("The Focus (MAP04)", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)))
|
||||
set_rule(world.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Red", player), lambda state:
|
||||
state.has("The Focus (MAP04) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Blue", player), lambda state:
|
||||
state.has("The Focus (MAP04) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Focus (MAP04) Yellow -> The Focus (MAP04) Red", player), lambda state:
|
||||
state.has("The Focus (MAP04) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Yellow", player), lambda state:
|
||||
state.has("The Focus (MAP04) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Main", player), lambda state:
|
||||
state.has("The Focus (MAP04) - Red keycard", player, 1))
|
||||
|
||||
# The Waste Tunnels (MAP05)
|
||||
set_rule(world.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state:
|
||||
(state.has("The Waste Tunnels (MAP05)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Yellow", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Main", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
|
||||
|
||||
# The Crusher (MAP06)
|
||||
set_rule(world.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state:
|
||||
(state.has("The Crusher (MAP06)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Crusher (MAP06) Yellow -> The Crusher (MAP06) Red", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Yellow", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Blue", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Red keycard", player, 1))
|
||||
|
||||
# Dead Simple (MAP07)
|
||||
set_rule(world.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state:
|
||||
(state.has("Dead Simple (MAP07)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
# Tricks and Traps (MAP08)
|
||||
set_rule(world.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state:
|
||||
(state.has("Tricks and Traps (MAP08)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Red", player), lambda state:
|
||||
state.has("Tricks and Traps (MAP08) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Yellow", player), lambda state:
|
||||
state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1))
|
||||
|
||||
# The Pit (MAP09)
|
||||
set_rule(world.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state:
|
||||
(state.has("The Pit (MAP09)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
|
||||
|
||||
# Refueling Base (MAP10)
|
||||
set_rule(world.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state:
|
||||
(state.has("Refueling Base (MAP10)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state:
|
||||
state.has("Refueling Base (MAP10) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state:
|
||||
state.has("Refueling Base (MAP10) - Blue keycard", player, 1))
|
||||
|
||||
# Circle of Death (MAP11)
|
||||
set_rule(world.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state:
|
||||
(state.has("Circle of Death (MAP11)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Blue", player), lambda state:
|
||||
state.has("Circle of Death (MAP11) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Red", player), lambda state:
|
||||
state.has("Circle of Death (MAP11) - Red keycard", player, 1))
|
||||
|
||||
|
||||
def set_episode2_rules(player, world, pro):
|
||||
# The Factory (MAP12)
|
||||
set_rule(world.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state:
|
||||
(state.has("The Factory (MAP12)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state:
|
||||
state.has("The Factory (MAP12) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state:
|
||||
state.has("The Factory (MAP12) - Blue keycard", player, 1))
|
||||
|
||||
# Downtown (MAP13)
|
||||
set_rule(world.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state:
|
||||
(state.has("Downtown (MAP13)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Yellow", player), lambda state:
|
||||
state.has("Downtown (MAP13) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Red", player), lambda state:
|
||||
state.has("Downtown (MAP13) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Blue", player), lambda state:
|
||||
state.has("Downtown (MAP13) - Blue keycard", player, 1))
|
||||
|
||||
# The Inmost Dens (MAP14)
|
||||
set_rule(world.get_entrance("Hub -> The Inmost Dens (MAP14) Main", player), lambda state:
|
||||
(state.has("The Inmost Dens (MAP14)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Inmost Dens (MAP14) Main -> The Inmost Dens (MAP14) Red", player), lambda state:
|
||||
state.has("The Inmost Dens (MAP14) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Inmost Dens (MAP14) Blue -> The Inmost Dens (MAP14) Red East", player), lambda state:
|
||||
state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Inmost Dens (MAP14) Red -> The Inmost Dens (MAP14) Main", player), lambda state:
|
||||
state.has("The Inmost Dens (MAP14) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Inmost Dens (MAP14) Red East -> The Inmost Dens (MAP14) Blue", player), lambda state:
|
||||
state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1))
|
||||
|
||||
# Industrial Zone (MAP15)
|
||||
set_rule(world.get_entrance("Hub -> Industrial Zone (MAP15) Main", player), lambda state:
|
||||
(state.has("Industrial Zone (MAP15)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow East", player), lambda state:
|
||||
state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow West", player), lambda state:
|
||||
state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("Industrial Zone (MAP15) Blue -> Industrial Zone (MAP15) Yellow East", player), lambda state:
|
||||
state.has("Industrial Zone (MAP15) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("Industrial Zone (MAP15) Yellow East -> Industrial Zone (MAP15) Blue", player), lambda state:
|
||||
state.has("Industrial Zone (MAP15) - Blue keycard", player, 1))
|
||||
|
||||
# Suburbs (MAP16)
|
||||
set_rule(world.get_entrance("Hub -> Suburbs (MAP16) Main", player), lambda state:
|
||||
(state.has("Suburbs (MAP16)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Red", player), lambda state:
|
||||
state.has("Suburbs (MAP16) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Blue", player), lambda state:
|
||||
state.has("Suburbs (MAP16) - Blue skull key", player, 1))
|
||||
|
||||
# Tenements (MAP17)
|
||||
set_rule(world.get_entrance("Hub -> Tenements (MAP17) Main", player), lambda state:
|
||||
(state.has("Tenements (MAP17)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Tenements (MAP17) Main -> Tenements (MAP17) Red", player), lambda state:
|
||||
state.has("Tenements (MAP17) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Yellow", player), lambda state:
|
||||
state.has("Tenements (MAP17) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Blue", player), lambda state:
|
||||
state.has("Tenements (MAP17) - Blue keycard", player, 1))
|
||||
|
||||
# The Courtyard (MAP18)
|
||||
set_rule(world.get_entrance("Hub -> The Courtyard (MAP18) Main", player), lambda state:
|
||||
(state.has("The Courtyard (MAP18)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Yellow", player), lambda state:
|
||||
state.has("The Courtyard (MAP18) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Blue", player), lambda state:
|
||||
state.has("The Courtyard (MAP18) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Courtyard (MAP18) Blue -> The Courtyard (MAP18) Main", player), lambda state:
|
||||
state.has("The Courtyard (MAP18) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Courtyard (MAP18) Yellow -> The Courtyard (MAP18) Main", player), lambda state:
|
||||
state.has("The Courtyard (MAP18) - Yellow skull key", player, 1))
|
||||
|
||||
# The Citadel (MAP19)
|
||||
set_rule(world.get_entrance("Hub -> The Citadel (MAP19) Main", player), lambda state:
|
||||
(state.has("The Citadel (MAP19)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("The Citadel (MAP19) Main -> The Citadel (MAP19) Red", player), lambda state:
|
||||
(state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Blue skull key", player, 1) or
|
||||
state.has("The Citadel (MAP19) - Yellow skull key", player, 1)))
|
||||
set_rule(world.get_entrance("The Citadel (MAP19) Red -> The Citadel (MAP19) Main", player), lambda state:
|
||||
(state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Yellow skull key", player, 1) or
|
||||
state.has("The Citadel (MAP19) - Blue skull key", player, 1)))
|
||||
|
||||
# Gotcha! (MAP20)
|
||||
set_rule(world.get_entrance("Hub -> Gotcha! (MAP20) Main", player), lambda state:
|
||||
(state.has("Gotcha! (MAP20)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
|
||||
def set_episode3_rules(player, world, pro):
|
||||
# Nirvana (MAP21)
|
||||
set_rule(world.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state:
|
||||
(state.has("Nirvana (MAP21)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
|
||||
|
||||
# The Catacombs (MAP22)
|
||||
set_rule(world.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
(state.has("The Catacombs (MAP22)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("BFG9000", player, 1) or
|
||||
state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
set_rule(world.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
|
||||
|
||||
# Barrels o Fun (MAP23)
|
||||
set_rule(world.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state:
|
||||
(state.has("Barrels o Fun (MAP23)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(world.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state:
|
||||
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state:
|
||||
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
|
||||
|
||||
# The Chasm (MAP24)
|
||||
set_rule(world.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state:
|
||||
state.has("The Chasm (MAP24)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(world.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Red keycard", player, 1))
|
||||
|
||||
# Bloodfalls (MAP25)
|
||||
set_rule(world.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(world.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
|
||||
|
||||
# The Abandoned Mines (MAP26)
|
||||
set_rule(world.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state:
|
||||
state.has("The Abandoned Mines (MAP26)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Yellow", player), lambda state:
|
||||
state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Red", player), lambda state:
|
||||
state.has("The Abandoned Mines (MAP26) - Red keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Blue", player), lambda state:
|
||||
state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Abandoned Mines (MAP26) Blue -> The Abandoned Mines (MAP26) Main", player), lambda state:
|
||||
state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1))
|
||||
set_rule(world.get_entrance("The Abandoned Mines (MAP26) Yellow -> The Abandoned Mines (MAP26) Main", player), lambda state:
|
||||
state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1))
|
||||
|
||||
# Monster Condo (MAP27)
|
||||
set_rule(world.get_entrance("Hub -> Monster Condo (MAP27) Main", player), lambda state:
|
||||
state.has("Monster Condo (MAP27)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Yellow", player), lambda state:
|
||||
state.has("Monster Condo (MAP27) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Red", player), lambda state:
|
||||
state.has("Monster Condo (MAP27) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Blue", player), lambda state:
|
||||
state.has("Monster Condo (MAP27) - Blue skull key", player, 1))
|
||||
set_rule(world.get_entrance("Monster Condo (MAP27) Red -> Monster Condo (MAP27) Main", player), lambda state:
|
||||
state.has("Monster Condo (MAP27) - Red skull key", player, 1))
|
||||
|
||||
# The Spirit World (MAP28)
|
||||
set_rule(world.get_entrance("Hub -> The Spirit World (MAP28) Main", player), lambda state:
|
||||
state.has("The Spirit World (MAP28)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(world.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Yellow", player), lambda state:
|
||||
state.has("The Spirit World (MAP28) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Red", player), lambda state:
|
||||
state.has("The Spirit World (MAP28) - Red skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Spirit World (MAP28) Yellow -> The Spirit World (MAP28) Main", player), lambda state:
|
||||
state.has("The Spirit World (MAP28) - Yellow skull key", player, 1))
|
||||
set_rule(world.get_entrance("The Spirit World (MAP28) Red -> The Spirit World (MAP28) Main", player), lambda state:
|
||||
state.has("The Spirit World (MAP28) - Red skull key", player, 1))
|
||||
|
||||
# The Living End (MAP29)
|
||||
set_rule(world.get_entrance("Hub -> The Living End (MAP29) Main", player), lambda state:
|
||||
state.has("The Living End (MAP29)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
|
||||
# Icon of Sin (MAP30)
|
||||
set_rule(world.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state:
|
||||
state.has("Icon of Sin (MAP30)", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
|
||||
|
||||
def set_episode4_rules(player, world, pro):
|
||||
# Wolfenstein2 (MAP31)
|
||||
set_rule(world.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state:
|
||||
(state.has("Wolfenstein2 (MAP31)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
# Grosse2 (MAP32)
|
||||
set_rule(world.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state:
|
||||
(state.has("Grosse2 (MAP32)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
|
||||
def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro):
|
||||
player = doom_ii_world.player
|
||||
world = doom_ii_world.multiworld
|
||||
|
||||
if included_episodes[0]:
|
||||
set_episode1_rules(player, world, pro)
|
||||
if included_episodes[1]:
|
||||
set_episode2_rules(player, world, pro)
|
||||
if included_episodes[2]:
|
||||
set_episode3_rules(player, world, pro)
|
||||
if included_episodes[3]:
|
||||
set_episode4_rules(player, world, pro)
|
||||
@@ -1,267 +0,0 @@
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from BaseClasses import Entrance, CollectionState, Item, Location, MultiWorld, Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Items, Locations, Maps, Regions, Rules
|
||||
from .Options import DOOM2Options
|
||||
|
||||
logger = logging.getLogger("DOOM II")
|
||||
|
||||
DOOM_TYPE_LEVEL_COMPLETE = -2
|
||||
DOOM_TYPE_COMPUTER_AREA_MAP = 2026
|
||||
|
||||
|
||||
class DOOM2Location(Location):
|
||||
game: str = "DOOM II"
|
||||
|
||||
|
||||
class DOOM2Item(Item):
|
||||
game: str = "DOOM II"
|
||||
|
||||
|
||||
class DOOM2Web(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the DOOM II randomizer connected to an Archipelago Multiworld",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Daivuk"]
|
||||
)]
|
||||
theme = "dirt"
|
||||
|
||||
|
||||
class DOOM2World(World):
|
||||
"""
|
||||
Doom II, also known as Doom II: Hell on Earth, is a first-person shooter game by id Software.
|
||||
It was released for MS-DOS in 1994.
|
||||
Compared to its predecessor, Doom II features larger levels, new enemies, a new "super shotgun" weapon
|
||||
"""
|
||||
options_dataclass = DOOM2Options
|
||||
options: DOOM2Options
|
||||
game = "DOOM II"
|
||||
web = DOOM2Web()
|
||||
data_version = 3
|
||||
required_client_version = (0, 3, 9)
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"Entryway (MAP01)",
|
||||
"The Factory (MAP12)",
|
||||
"Nirvana (MAP21)"
|
||||
]
|
||||
|
||||
# Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
|
||||
# The ratio have been tweaked seem, and feel good.
|
||||
items_ratio: Dict[str, float] = {
|
||||
"Armor": 41,
|
||||
"Mega Armor": 25,
|
||||
"Berserk": 12,
|
||||
"Invulnerability": 10,
|
||||
"Partial invisibility": 18,
|
||||
"Supercharge": 28,
|
||||
"Medikit": 15,
|
||||
"Box of bullets": 13,
|
||||
"Box of rockets": 13,
|
||||
"Box of shotgun shells": 13,
|
||||
"Energy cell pack": 10
|
||||
}
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0]
|
||||
self.location_count = 0
|
||||
|
||||
super().__init__(world, player)
|
||||
|
||||
def get_episode_count(self):
|
||||
# Don't include 4th, those are secret levels they are additive
|
||||
return sum(self.included_episodes[:3])
|
||||
|
||||
def generate_early(self):
|
||||
# Cache which episodes are included
|
||||
self.included_episodes[0] = self.options.episode1.value
|
||||
self.included_episodes[1] = self.options.episode2.value
|
||||
self.included_episodes[2] = self.options.episode3.value
|
||||
self.included_episodes[3] = self.options.episode4.value # 4th episode are secret levels
|
||||
|
||||
# If no episodes selected, select Episode 1
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
|
||||
# Main regions
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
hub_region = Region("Hub", self.player, self.multiworld)
|
||||
self.multiworld.regions += [menu_region, hub_region]
|
||||
menu_region.add_exits(["Hub"])
|
||||
|
||||
# Create regions and locations
|
||||
main_regions = []
|
||||
connections = []
|
||||
for region_dict in Regions.regions:
|
||||
if not self.included_episodes[region_dict["episode"] - 1]:
|
||||
continue
|
||||
|
||||
region_name = region_dict["name"]
|
||||
if region_dict["connects_to_hub"]:
|
||||
main_regions.append(region_name)
|
||||
|
||||
region = Region(region_name, self.player, self.multiworld)
|
||||
region.add_locations({
|
||||
loc["name"]: loc_id
|
||||
for loc_id, loc in Locations.location_table.items()
|
||||
if loc["region"] == region_name and self.included_episodes[loc["episode"] - 1]
|
||||
}, DOOM2Location)
|
||||
|
||||
self.multiworld.regions.append(region)
|
||||
|
||||
for connection_dict in region_dict["connections"]:
|
||||
# Check if it's a pro-only connection
|
||||
if connection_dict["pro"] and not pro:
|
||||
continue
|
||||
connections.append((region, connection_dict["target"]))
|
||||
|
||||
# Connect main regions to Hub
|
||||
hub_region.add_exits(main_regions)
|
||||
|
||||
# Do the other connections between regions (They are not all both ways)
|
||||
for connection in connections:
|
||||
source = connection[0]
|
||||
target = self.multiworld.get_region(connection[1], self.player)
|
||||
|
||||
entrance = Entrance(self.player, f"{source.name} -> {target.name}", source)
|
||||
source.exits.append(entrance)
|
||||
entrance.connect(target)
|
||||
|
||||
# Sum locations for items creation
|
||||
self.location_count = len(self.multiworld.get_locations(self.player))
|
||||
|
||||
def completion_rule(self, state: CollectionState):
|
||||
for map_name in Maps.map_names:
|
||||
if map_name + " - Exit" not in self.location_name_to_id:
|
||||
continue
|
||||
|
||||
# Exit location names are in form: Entryway (MAP01) - Exit
|
||||
loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]]
|
||||
if not self.included_episodes[loc["episode"] - 1]:
|
||||
continue
|
||||
|
||||
# Map complete item names are in form: Entryway (MAP01) - Complete
|
||||
if not state.has(map_name + " - Complete", self.player, 1):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def set_rules(self):
|
||||
pro = self.options.pro.value
|
||||
allow_death_logic = self.options.allow_death_logic.value
|
||||
|
||||
Rules.set_rules(self, self.included_episodes, pro)
|
||||
self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state)
|
||||
|
||||
# Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed
|
||||
# platform) Unless the user allows for it.
|
||||
if not allow_death_logic:
|
||||
for death_logic_location in Locations.death_logic_locations:
|
||||
self.multiworld.exclude_locations[self.player].value.add(death_logic_location)
|
||||
|
||||
def create_item(self, name: str) -> DOOM2Item:
|
||||
item_id: int = self.item_name_to_id[name]
|
||||
return DOOM2Item(name, Items.item_table[item_id]["classification"], item_id, self.player)
|
||||
|
||||
def create_items(self):
|
||||
itempool: List[DOOM2Item] = []
|
||||
start_with_computer_area_maps: bool = self.options.start_with_computer_area_maps.value
|
||||
|
||||
# Items
|
||||
for item_id, item in Items.item_table.items():
|
||||
if item["doom_type"] == DOOM_TYPE_LEVEL_COMPLETE:
|
||||
continue # We'll fill it manually later
|
||||
|
||||
if item["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and start_with_computer_area_maps:
|
||||
continue # We'll fill it manually, and we will put fillers in place
|
||||
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
item_name = map_name + " - Complete"
|
||||
|
||||
if loc_name not in self.location_name_to_id:
|
||||
continue
|
||||
|
||||
if item_name not in self.item_name_to_id:
|
||||
continue
|
||||
|
||||
loc = Locations.location_table[self.location_name_to_id[loc_name]]
|
||||
if not self.included_episodes[loc["episode"] - 1]:
|
||||
continue
|
||||
|
||||
self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name))
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.starting_level_for_episode)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if start_with_computer_area_maps:
|
||||
for item_id, item_dict in Items.item_table.items():
|
||||
item_episode = item_dict["episode"]
|
||||
if item_episode > 0:
|
||||
if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]:
|
||||
self.multiworld.push_precollected(self.create_item(item_dict["name"]))
|
||||
|
||||
# Fill the rest starting with powerups, then fillers
|
||||
self.create_ratioed_items("Armor", itempool)
|
||||
self.create_ratioed_items("Mega Armor", itempool)
|
||||
self.create_ratioed_items("Berserk", itempool)
|
||||
self.create_ratioed_items("Invulnerability", itempool)
|
||||
self.create_ratioed_items("Partial invisibility", itempool)
|
||||
self.create_ratioed_items("Supercharge", itempool)
|
||||
|
||||
while len(itempool) < self.location_count:
|
||||
itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
# add itempool to multiworld
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def get_filler_item_name(self):
|
||||
return self.multiworld.random.choice([
|
||||
"Medikit",
|
||||
"Box of bullets",
|
||||
"Box of rockets",
|
||||
"Box of shotgun shells",
|
||||
"Energy cell pack"
|
||||
])
|
||||
|
||||
def create_ratioed_items(self, item_name: str, itempool: List[DOOM2Item]):
|
||||
remaining_loc = self.location_count - len(itempool)
|
||||
ep_count = self.get_episode_count()
|
||||
|
||||
# Was balanced based on DOOM 1993's first 3 episodes
|
||||
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
|
||||
if count == 0:
|
||||
logger.warning("Warning, no ", item_name, " will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
itempool.append(self.create_item(item_name))
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
|
||||
@@ -1,23 +0,0 @@
|
||||
# DOOM II
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page](../player-settings) contains the options needed to configure your game session.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Guns, keycards, and level unlocks have been randomized. Typically, you will end up playing different levels out of order to find your keycards and level unlocks and eventually complete your game.
|
||||
|
||||
Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level.
|
||||
|
||||
## What is the goal?
|
||||
|
||||
The goal is to complete every level.
|
||||
|
||||
## What is a "check" in DOOM II?
|
||||
|
||||
Guns, keycards, and powerups have been replaced with Archipelago checks. The switch at the end of each level is also a check.
|
||||
|
||||
## What "items" can you unlock in DOOM II?
|
||||
|
||||
Keycards and level unlocks are your main progression items. Gun unlocks and some upgrades are your useful items. Temporary powerups, ammo, healing, and armor are filler items.
|
||||
@@ -1,51 +0,0 @@
|
||||
# DOOM II Randomizer Setup
|
||||
|
||||
## Required Software
|
||||
|
||||
- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/)
|
||||
- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases)
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
## Installing AP Doom
|
||||
1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it.
|
||||
2. Copy DOOM2.WAD from your steam install into the extracted folder.
|
||||
You can find the folder in steam by finding the game in your library,
|
||||
right clicking it and choosing *Manage→Browse Local Files*.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Launch apdoom-launcher.exe
|
||||
2. Select `DOOM II` from the drop-down
|
||||
3. Enter the Archipelago server address, slot name, and password (if you have one)
|
||||
4. Press "Launch DOOM"
|
||||
5. Enjoy!
|
||||
|
||||
To continue a game, follow the same connection steps.
|
||||
Connecting with a different seed won't erase your progress in other seeds.
|
||||
|
||||
## Archipelago Text Client
|
||||
|
||||
We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send.
|
||||
APDOOM has in-game messages,
|
||||
but they disappear quickly and there's no reasonable way to check your message history in-game.
|
||||
|
||||
### Hinting
|
||||
|
||||
To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM II can be difficult because names are rather long and contain special characters. For example:
|
||||
```
|
||||
!hint Underhalls (MAP02) - Red keycard
|
||||
```
|
||||
The game has a hint helper implemented, where you can simply type this:
|
||||
```
|
||||
!hint map02 red
|
||||
```
|
||||
For this to work, include the map short name (`MAP01`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`.
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
APDOOM has a functional map tracker integrated into the level select screen.
|
||||
It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed,
|
||||
and how many of the checks you have completed in each level.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user