diff --git a/MultiServer.py b/MultiServer.py index 02dabe9e232d..fe26361d6800 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2,8 +2,8 @@ import argparse import asyncio -import copy import collections +import copy import datetime import functools import hashlib @@ -955,7 +955,16 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi logging.info('(Team #%d) %s sent %s to %s (%s)' % ( team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], ctx.player_names[(team, target_player)], ctx.location_names[location])) - info_text = json_format_send_event(new_item, target_player) + + if ctx.item_names[item_id].endswith(" Stone"): + info_text = json_format_stone_event(new_item, target_player) + elif ctx.item_names[item_id].startswith("the dusted"): + info_text = json_format_dust_event(new_item, target_player) + elif ctx.item_names[item_id] == "Triforce Piece": + info_text = json_format_triforce_event(new_item, target_player) + else: + info_text = json_format_send_event(new_item, target_player) + ctx.broadcast_team(team, [info_text]) ctx.location_checks[team, slot] |= new_locations @@ -1036,6 +1045,46 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int): "item": net_item} +def json_format_stone_event(net_item: NetworkItem, receiving_player: int): + parts = [] + NetUtils.add_json_text(parts, net_item, type=NetUtils.JSONTypes.player_id) + NetUtils.add_json_text(parts, " found the ") + NetUtils.add_json_item(parts, net_item.item, net_item.player, net_item.flags) + NetUtils.add_json_text(parts, " (") + NetUtils.add_json_location(parts, net_item.location, net_item.player) + NetUtils.add_json_text(parts, ")") + + return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend", + "receiving": receiving_player, + "item": net_item} + + +def json_format_triforce_event(net_item: NetworkItem, receiving_player: int): + parts = [] + NetUtils.add_json_text(parts, net_item, type=NetUtils.JSONTypes.player_id) + NetUtils.add_json_text(parts, " also found a ") + NetUtils.add_json_item(parts, net_item.item, net_item.player, net_item.flags) + NetUtils.add_json_text(parts, "taped to the back of the stone!") + + return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend", + "receiving": receiving_player, + "item": net_item} + + +def json_format_dust_event(net_item: NetworkItem, receiving_player: int): + parts = [] + NetUtils.add_json_text(parts, net_item, type=NetUtils.JSONTypes.player_id) + NetUtils.add_json_text(parts, " found ") + NetUtils.add_json_item(parts, net_item.item, net_item.player, net_item.flags) + NetUtils.add_json_text(parts, ". (") + NetUtils.add_json_location(parts, net_item.location, net_item.player) + NetUtils.add_json_text(parts, ")") + + return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend", + "receiving": receiving_player, + "item": net_item} + + def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: picks = Utils.get_fuzzy_results(input_text, possible_answers, limit=2) if len(picks) > 1: diff --git a/WebHostLib/static/assets/gauntletTracker.js b/WebHostLib/static/assets/gauntletTracker.js new file mode 100644 index 000000000000..a698214b8dd6 --- /dev/null +++ b/WebHostLib/static/assets/gauntletTracker.js @@ -0,0 +1,49 @@ +window.addEventListener('load', () => { + // Reload tracker every 15 seconds + const url = window.location; + setInterval(() => { + const ajax = new XMLHttpRequest(); + ajax.onreadystatechange = () => { + if (ajax.readyState !== 4) { return; } + + // Create a fake DOM using the returned HTML + const domParser = new DOMParser(); + const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html'); + + // Update item tracker + document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML; + // Update only counters in the location-table + let counters = document.getElementsByClassName('counter'); + const fakeCounters = fakeDOM.getElementsByClassName('counter'); + for (let i = 0; i < counters.length; i++) { + counters[i].innerHTML = fakeCounters[i].innerHTML; + } + }; + ajax.open('GET', url); + ajax.send(); + }, 15000) + + // Collapsible advancement sections + const categories = document.getElementsByClassName("location-category"); + for (let i = 0; i < categories.length; i++) { + let hide_id = categories[i].id.split('-')[0]; + if (hide_id == 'Total') { + continue; + } + categories[i].addEventListener('click', function() { + // Toggle the advancement list + document.getElementById(hide_id).classList.toggle("hide"); + // Change text of the header + const tab_header = document.getElementById(hide_id+'-header').children[0]; + const orig_text = tab_header.innerHTML; + let new_text; + if (orig_text.includes("▼")) { + new_text = orig_text.replace("▼", "▲"); + } + else { + new_text = orig_text.replace("▲", "▼"); + } + tab_header.innerHTML = new_text; + }); + } +}); diff --git a/WebHostLib/static/static/backgrounds/wallpaper.jpg b/WebHostLib/static/static/backgrounds/wallpaper.jpg new file mode 100644 index 000000000000..811e2082eaeb Binary files /dev/null and b/WebHostLib/static/static/backgrounds/wallpaper.jpg differ diff --git a/WebHostLib/static/styles/gauntletTracker.css b/WebHostLib/static/styles/gauntletTracker.css new file mode 100644 index 000000000000..224cdcdc55a0 --- /dev/null +++ b/WebHostLib/static/styles/gauntletTracker.css @@ -0,0 +1,102 @@ +#player-tracker-wrapper{ + margin: 0; +} + +#inventory-table{ + border-top: 2px solid #000000; + border-left: 2px solid #000000; + border-right: 2px solid #000000; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 3px 3px 10px; + width: 384px; + background-color: #42b149; +} + +#inventory-table td{ + width: 40px; + height: 40px; + text-align: center; + vertical-align: middle; +} + +#inventory-table img{ + height: 100%; + max-width: 40px; + max-height: 40px; + filter: grayscale(100%) contrast(75%) brightness(30%); +} + +#inventory-table img.acquired{ + filter: none; +} + +#inventory-table div.counted-item { + position: relative; +} + +#inventory-table div.item-count { + position: absolute; + color: white; + font-family: "Minecraftia", monospace; + font-weight: bold; + bottom: 0; + right: 0; +} + +#location-table{ + width: 384px; + border-left: 2px solid #000000; + border-right: 2px solid #000000; + border-bottom: 2px solid #000000; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + background-color: #42b149; + padding: 0 3px 3px; + font-family: "Minecraftia", monospace; + font-size: 14px; + cursor: default; +} + +#location-table th{ + vertical-align: middle; + text-align: left; + padding-right: 10px; +} + +#location-table td{ + padding-top: 2px; + padding-bottom: 2px; + line-height: 20px; +} + +#location-table td.counter { + text-align: right; + font-size: 14px; +} + +#location-table td.toggle-arrow { + text-align: right; +} + +#location-table tr#Total-header { + font-weight: bold; +} + +#location-table img{ + height: 100%; + max-width: 30px; + max-height: 30px; +} + +#location-table tbody.locations { + font-size: 12px; +} + +#location-table td.location-name { + padding-left: 16px; +} + +.hide { + display: none; +} diff --git a/WebHostLib/templates/gauntletTracker.html b/WebHostLib/templates/gauntletTracker.html new file mode 100644 index 000000000000..86bb27485bc9 --- /dev/null +++ b/WebHostLib/templates/gauntletTracker.html @@ -0,0 +1,85 @@ + + + + The Infinity Gauntlet + + + + +
+

Found Pieces:

+
+ + + + + + +
+
+ + diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 8f9fb1488122..28a1f410c20f 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1,7 +1,7 @@ import collections import datetime import typing -from typing import Counter, Optional, Dict, Any, Tuple +from typing import Any, Counter, Dict, Optional, Tuple from uuid import UUID from flask import render_template @@ -565,6 +565,28 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) +def __renderGauntletTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], + inventory: Counter, team: int, player: int, playerName: str, + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, + saving_second: int) -> str: + + icons = { + "Space Stone": "https://static.wikia.nocookie.net/marvelcinematicuniverse/images/0/0a/Space_Stone_VFX.png", + "Mind Stone": "https://static.wikia.nocookie.net/marvelcinematicuniverse/images/e/e4/Mind_Stone_VFX.png", + "Reality Stone": "https://static.wikia.nocookie.net/marvelcinematicuniverse/images/9/9b/Reality_Stone_VFX.png", + "Power Stone": "https://static.wikia.nocookie.net/marvelcinematicuniverse/images/d/d7/Power_Stone_VFX.png", + "Time Stone": "https://static.wikia.nocookie.net/marvelcinematicuniverse/images/f/f0/Time_Stone_VFX.png", + "Soul Stone": "https://static.wikia.nocookie.net/marvelcinematicuniverse/images/1/17/Soul_Stone_VFX.png", + "Wallpaper": "https://c.wallhere.com/photos/fe/3f/The_Legend_of_Zelda_video_games_Nintendo_The_Legend_of_Zelda_Majora's_Mask-223870.jpg!d", + } + + return render_template("gauntletTracker.html", + inventory=inventory, icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id in inventory if + id in lookup_any_item_id_to_name}, + player=player, team=team, room=room, player_name=playerName, saving_second = saving_second, + checks_done=checks_done, checks_in_area=checks_in_area) + def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, @@ -1574,7 +1596,8 @@ def attribute_item(team: int, recipient: int, item: int): "A Link to the Past": __renderAlttpTracker, "ChecksFinder": __renderChecksfinder, "Super Metroid": __renderSuperMetroidTracker, - "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker + "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker, + "Infinity Gauntlet": __renderGauntletTracker, } multi_trackers: typing.Dict[str, typing.Callable] = { diff --git a/host.yaml b/host.yaml index c2647c44caae..57ea591cec78 100644 --- a/host.yaml +++ b/host.yaml @@ -60,7 +60,7 @@ generator: # Folder from which the player yaml files are pulled from player_files_path: "Players" #amount of players, 0 to infer from player files - players: 0 + players: 5 # general weights file, within the stated player_files_path location # gets used if players is higher than the amount of per-player files found to fill remaining slots weights_file_path: "weights.yaml" diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3f68e34b3f3c..8bfd661eafd0 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -5,24 +5,24 @@ import typing import Utils -from BaseClasses import Item, CollectionState, Tutorial, MultiWorld -from .Dungeons import create_dungeons, Dungeon -from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ - indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted +from BaseClasses import CollectionState, Item, ItemClassification, MultiWorld, Tutorial +from worlds.AutoWorld import LogicMixin, WebWorld, World +from .Client import ALTTPSNIClient +from .Dungeons import Dungeon, create_dungeons +from .EntranceShuffle import indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted, \ + link_entrances, link_inverted_entrances, plando_connect from .InvertedRegions import create_inverted_regions, mark_dark_world_regions -from .ItemPool import generate_itempool, difficulties -from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem +from .ItemPool import difficulties, generate_itempool +from .Items import GetBeemizerItem, item_init_table, item_name_groups, item_table from .Options import alttp_options, smallkey_shuffle -from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ - is_main_entrance -from .Client import ALTTPSNIClient -from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ - get_hash_string, get_base_rom_path, LttPDeltaPatch +from .Regions import create_regions, is_main_entrance, lookup_name_to_id, lookup_vanilla_location_to_entrance, \ + mark_light_world_regions +from .Rom import LocalRom, LttPDeltaPatch, apply_rom_settings, check_enemizer, get_base_rom_path, get_hash_string, \ + patch_enemizer, patch_race_rom, patch_rom from .Rules import set_rules -from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name -from .SubClasses import ALttPItem, LTTPRegionType -from worlds.AutoWorld import World, WebWorld, LogicMixin +from .Shops import Shop, ShopSlotFill, ShopType, create_shops, price_rate_display, price_type_display_name from .StateHelpers import can_buy_unlimited +from .SubClasses import ALttPItem, LTTPRegionType lttp_logger = logging.getLogger("A Link to the Past") @@ -230,6 +230,8 @@ class ALTTPWorld(World): has_progressive_bows: bool dungeons: typing.Dict[str, Dungeon] + counter: typing.ClassVar[int] = 0 + def __init__(self, *args, **kwargs): self.dungeon_local_item_names = set() self.dungeon_specific_item_names = set() @@ -244,7 +246,7 @@ def stage_assert_generate(cls, multiworld: MultiWorld): if not os.path.exists(rom_file): raise FileNotFoundError(rom_file) if multiworld.is_race: - import xxtea + pass for player in multiworld.get_game_players(cls.game): if multiworld.worlds[player].use_enemizer: check_enemizer(multiworld.worlds[player].enemizer_path) @@ -460,10 +462,103 @@ def pre_fill(self): raise FillError('Unable to place dungeon prizes') @classmethod - def stage_pre_fill(cls, world): + def stage_pre_fill(cls, world: MultiWorld): from .Dungeons import fill_dungeons_restrictive fill_dungeons_restrictive(world) + # Thanos did a sneaky. + from worlds.infinity_gauntlet import IGItem, IGWorld + + replacement_table: typing.Dict[str, str] = { + "Single Arrow": "the dusted remains of a single arrow", + "Arrows (10)": "the dusted remains of 10 arrows", + "Single Bomb": "the dusted remains of a single bomb", + "Bombs (3)": "the dusted remains of 3 bombs", + "Bombs (10)": "the dusted remains of 10 bombs", + "Boss Heart Container": "the dusted remains of a Boss Heart Container", + "Sanctuary Heart Container": "the dusted remains of a Sanctuary Heart Container", + "Piece of Heart": "the dusted remains of a Piece of Heart", + "Rupee (1)": "the dusted remains of a green rupee", + "Rupees (5)": "the dusted remains of a blue rupee", + "Rupees (20)": "the dusted remains of a red rupee", + "Rupees (50)": "the dusted remains of a purple rupee", + "Rupees (100)": "the dusted remains of an orange rupee", + "Rupees (300)": "the dusted remains of a silver rupee", + "Bee": "the dusted remains of a bee", + "Bee Trap": "the dusted remains of a bunch of bees", + } + + prog_replacement_table: typing.Dict[str, str] = { + "Progressive Mail": "the dusted remains of some Red Mail", + "Progressive Shield": "the dusted remains of a Mirror Shield", + "Progressive Sword": "the dusted remains of a sword", + } + + prog_item_snapped: typing.Dict[str, typing.List[int]] = { + "Progressive Sword": [], + "Progressive Mail": [], + "Progressive Shield": [], + } + + piece_index = 0 + stones = [ + "Taped to the Space Stone", + "Taped to the Reality Stone", + "Taped to the Power Stone", + "Taped to the Soul Stone", + "Taped to the Mind Stone", + "Taped to the Time Stone", + ] + + infinity_player = [ig_world for ig_world in world.worlds.values() if isinstance(ig_world, IGWorld)][0].player + new_item_pool: typing.List[Item] = [] + for item in world.itempool: + # Don't want to touch our beautiful items. + if isinstance(item, IGItem): + new_item_pool.append(item) + continue + + if item.name == "Triforce Piece": + world.get_location(stones[piece_index], infinity_player).place_locked_item(item) + piece_index += 1 + continue + + if item.name in prog_item_snapped.keys(): + if item.player not in prog_item_snapped[item.name]: + prog_item_snapped[item.name].append(item.player) + item_id = IGWorld.item_name_to_id[prog_replacement_table[item.name]] + item = IGItem(prog_replacement_table[item.name], ItemClassification.trap, item_id, infinity_player) + new_item_pool.append(item) + else: + new_item_pool.append(item) + + continue + + # 50% Chance for Item to get Thanos snapped + if item.name in replacement_table.keys(): + if world.random.choice([True, False]): + item_id = IGWorld.item_name_to_id[replacement_table[item.name]] + item = IGItem(replacement_table[item.name], ItemClassification.trap, item_id, infinity_player) + new_item_pool.append(item) + continue + else: + new_item_pool.append(item) + continue + + if item.name.startswith("Map "): + item = IGItem("the dusted remains of a map", ItemClassification.trap, 69898999, infinity_player) + new_item_pool.append(item) + continue + + if item.name.startswith("Compass "): + item = IGItem("the dusted remains of a compass", ItemClassification.trap, 69898998, infinity_player) + new_item_pool.append(item) + continue + + new_item_pool.append(item) + + world.itempool = new_item_pool + @classmethod def stage_post_fill(cls, world): ShopSlotFill(world) diff --git a/worlds/infinity_gauntlet/__init__.py b/worlds/infinity_gauntlet/__init__.py new file mode 100644 index 000000000000..d46f31da8c58 --- /dev/null +++ b/worlds/infinity_gauntlet/__init__.py @@ -0,0 +1,106 @@ +from BaseClasses import CollectionState, Item, ItemClassification, Location, Region +from worlds.AutoWorld import World + + +class IGItem(Item): + game = "Infinity Gauntlet" + + +class IGLocation(Location): + game = "Infinity Gauntlet" + + +class IGWorld(World): + """There's literally no reason to be looking up this game.""" + game = "Infinity Gauntlet" + data_version = 0 + hidden = True + + location_name_to_id = { + "Taped to the Space Stone": 69_888_000, + "Taped to the Reality Stone": 69_888_001, + "Taped to the Power Stone": 69_888_002, + "Taped to the Soul Stone": 69_888_003, + "Taped to the Mind Stone": 69_888_004, + "Taped to the Time Stone": 69_888_005, + } + + item_name_to_id = { + "Space Stone": 69_888_000, + "Reality Stone": 69_888_001, + "Power Stone": 69_888_002, + "Soul Stone": 69_888_003, + "Mind Stone": 69_888_004, + "Time Stone": 69_888_005, + + # "Snapped" Items + "the dusted remains of a single arrow": 69_889_000 + 0x43, + "the dusted remains of 10 arrows": 69_889_000 + 0x44, + "the dusted remains of a single bomb": 69_889_000 + 0x27, + "the dusted remains of 3 bombs": 69_889_000 + 0x28, + "the dusted remains of 10 bombs": 69_889_000 + 0x31, + "the dusted remains of some Red Mail": 69_889_000 + 0x60, + "the dusted remains of a Mirror Shield": 69_889_000 + 0x5F, + "the dusted remains of a Boss Heart Container": 69_889_000 + 0x3E, + "the dusted remains of a Sanctuary Heart Container": 69_889_000 + 0x3F, + "the dusted remains of a Piece of Heart": 69_889_000 + 0x17, + "the dusted remains of a green rupee": 69_889_000 + 0x34, + "the dusted remains of a blue rupee": 69_889_000 + 0x35, + "the dusted remains of a red rupee": 69_889_000 + 0x36, + "the dusted remains of a purple rupee": 69_889_000 + 0x41, + "the dusted remains of an orange rupee": 69_889_000 + 0x40, + "the dusted remains of a silver rupee": 69_889_000 + 0x46, + "the dusted remains of a map": 69_889_000 + 9999, + "the dusted remains of a compass": 69_889_000 + 9998, + "the dusted remains of a bee": 69_889_000 + 0x0E, + "the dusted remains of a bunch of bees": 69_889_000 + 0xB0, + "the dusted remains of a sword": 69_889_000 + 0x5E, + } + + def create_item(self, name: str) -> IGItem: + classification = ItemClassification.progression_skip_balancing + if name.startswith("The dusted"): + classification = ItemClassification.trap + + return IGItem(name, classification, self.item_name_to_id[name], self.player) + + def create_regions(self): + menu_region = Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu_region) + + menu_region.add_locations(self.location_name_to_id, IGLocation) + + def get_filler_item_name(self) -> str: + return "Nothing" + + def has_all_stones(self, state: CollectionState): + return state.has_all( + set([item for item in self.item_name_to_id.keys() if not item.startswith("The dusted")]), + self.player + ) + + def set_rules(self): + self.multiworld.get_location("Taped to the Space Stone", self.player).access_rule = lambda state: state.has( + "Space Stone", self.player) + self.multiworld.get_location("Taped to the Reality Stone", self.player).access_rule = lambda state: state.has( + "Reality Stone", self.player) + self.multiworld.get_location("Taped to the Power Stone", self.player).access_rule = lambda state: state.has( + "Power Stone", self.player) + self.multiworld.get_location("Taped to the Soul Stone", self.player).access_rule = lambda state: state.has( + "Soul Stone", self.player) + self.multiworld.get_location("Taped to the Mind Stone", self.player).access_rule = lambda state: state.has( + "Mind Stone", self.player) + self.multiworld.get_location("Taped to the Time Stone", self.player).access_rule = lambda state: state.has( + "Time Stone", self.player) + + self.multiworld.completion_condition[self.player] = lambda state: self.has_all_stones(state) + + def create_items(self): + self.multiworld.itempool += [ + self.create_item("Space Stone"), + self.create_item("Reality Stone"), + self.create_item("Power Stone"), + self.create_item("Soul Stone"), + self.create_item("Mind Stone"), + self.create_item("Time Stone"), + ]