diff --git a/WebHostLib/static/styles/ootTracker.css b/WebHostLib/static/styles/ootTracker.css new file mode 100644 index 0000000000..579ed37716 --- /dev/null +++ b/WebHostLib/static/styles/ootTracker.css @@ -0,0 +1,136 @@ +#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: 448px; + background-color: rgb(60, 114, 157); +} + +#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: monospace; + font-weight: bold; + font-size: 1.1em; + bottom: 0px; + right: 8px; +} + +#location-table{ + width: 448px; + 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: rgb(60, 114, 157); + padding: 0 3px 3px; + font-family: monospace; + font-size: 15px; + 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: 15px; +} + +#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: 13px; +} + +#location-table td.location-name { + padding-left: 16px; +} + +.hide { + display: none; +} + +.right-align { + text-align: right; + font-weight: bold; +} + +#location-table td:first-child { + width: 272px; +} + +.location-category td:first-child { + padding-right: 16px; +} + +#inventory-table img.acquired#lullaby{ + filter: sepia(100%) hue-rotate(-60deg); /* css trick to hue-shift a static image */ +} + +#inventory-table img.acquired#epona{ + filter: sepia(100%) hue-rotate(-20deg) saturate(250%); +} + +#inventory-table img.acquired#saria{ + filter: sepia(100%) hue-rotate(60deg) saturate(150%); +} + +#inventory-table img.acquired#sun{ + filter: sepia(100%) hue-rotate(15deg) saturate(200%) brightness(120%); +} + +#inventory-table img.acquired#time{ + filter: sepia(100%) hue-rotate(160deg) saturate(150%); +} diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/ootTracker.html new file mode 100644 index 0000000000..64d61ae72d --- /dev/null +++ b/WebHostLib/templates/ootTracker.html @@ -0,0 +1,180 @@ + + + + {{ player_name }}'s Tracker + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
{{ hookshot_length }}
+
+
+
+ +
{{ bottle_count if bottle_count > 0 else '' }}
+
+
+
+ +
{{ wallet_size }}
+
+
+
+ +
Zelda
+
+
+
+ +
Epona
+
+
+
+ +
Saria
+
+
+
+ +
Sun
+
+
+
+ +
Time
+
+
+
+ +
Storms
+
+
+
+ +
{{ token_count }}
+
+
+
+ +
Min
+
+
+
+ +
Bol
+
+
+
+ +
Ser
+
+
+
+ +
Req
+
+
+
+ +
Noc
+
+
+
+ +
Pre
+
+
+
+ +
{{ piece_count if piece_count > 0 else '' }}
+
+
+ + + + + + + + {% for area in checks_done %} + + + + + + + + {% for location in location_info[area] %} + + + + + + + {% endfor %} + + {% endfor %} +
Items
{{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
+
+ + diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 624335e0b8..b34c520d12 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -524,6 +524,184 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) + elif games[tracked_player] == "Ocarina of Time": + oot_icons = { + "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png", + "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png", + "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png", + "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png", + "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png", + "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png", + "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png", + "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png", + "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png", + "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png", + "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png", + "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png", + "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png", + "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png", + "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png", + "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png", + "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png", + "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png", + "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png", + "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png", + "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png", + "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png", + "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png", + "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png", + "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png", + "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png", + "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png", + "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png", + "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png", + "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png", + "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png", + "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png", + "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png", + "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png", + "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png", + "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png", + "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png", + "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png", + "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png", + "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png", + "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png", + "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png", + "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png", + "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png", + "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png", + "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png", + "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png", + } + + display_data = {} + + # Determine display for progressive items + progressive_items = { + "Progressive Hookshot": 66128, + "Progressive Strength Upgrade": 66129, + "Progressive Wallet": 66133, + "Progressive Scale": 66134, + "Magic Meter": 66138, + "Ocarina": 66139, + } + progressive_names = { + "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"], + "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"], + "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"], + "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"], + "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"], + "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"] + } + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name])-1) + display_name = progressive_names[item_name][level] + if item_name.startswith("Progressive"): + base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_') + else: + base_name = item_name.lower().replace(' ', '_') + display_data[base_name+"_url"] = oot_icons[display_name] + + if base_name == "hookshot": + display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) + if base_name == "wallet": + display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) + + # Determine display for bottles. Show letter if it's obtained, determine bottle count + bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148] + display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4) + display_data['bottle_url'] = oot_icons['Rutos Letter'] if inventory[66021] > 0 else oot_icons['Bottle'] + + # Determine bombchu display + display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137])) + + # Multi-items + multi_items = { + "Gold Skulltula Token": 66091, + "Triforce Piece": 66202, + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + display_data[base_name+"_count"] = inventory[item_id] + + # Gather dungeon locations + area_id_ranges = { + "Overworld": (67000, 67280), + "Deku Tree": (67281, 67303), + "Dodongo's Cavern": (67304, 67334), + "Jabu Jabu's Belly": (67335, 67359), + "Bottom of the Well": (67360, 67384), + "Forest Temple": (67385, 67420), + "Fire Temple": (67421, 67457), + "Water Temple": (67458, 67484), + "Shadow Temple": (67485, 67532), + "Spirit Temple": (67533, 67582), + "Ice Cavern": (67583, 67596), + "Gerudo Training Grounds": (67597, 67635), + "Ganon's Castle": (67636, 67673), + } + def lookup_and_trim(id, area): + full_name = lookup_any_location_id_to_name[id] + if id == 67673: + return full_name[13:] # Ganons Tower Boss Key Chest + if area != 'Overworld': + return full_name[len(area):] # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC + return full_name + + checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).intersection(set(locations[tracked_player])) + location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[tracked_player]} + for area, (min_id, max_id) in area_id_ranges.items()} + checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges} + checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[tracked_player]]) + for area, (min_id, max_id) in area_id_ranges.items()} + checks_done['Total'] = sum(checks_done.values()) + checks_in_area['Total'] = sum(checks_in_area.values()) + + # Give skulltulas on non-tracked locations + non_tracked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).difference(set(locations[tracked_player])) + for id in non_tracked_locations: + if "GS" in lookup_and_trim(id, ''): + display_data["token_count"] += 1 + + # Gather small and boss key info + small_key_counts = { + "Forest Temple": inventory[66175], + "Fire Temple": inventory[66176], + "Water Temple": inventory[66177], + "Spirit Temple": inventory[66178], + "Shadow Temple": inventory[66179], + "Bottom of the Well": inventory[66180], + "Gerudo Training Grounds": inventory[66181], + "Ganon's Castle": inventory[66183], + } + boss_key_counts = { + "Forest Temple": '✔' if inventory[66149] else '✕', + "Fire Temple": '✔' if inventory[66150] else '✕', + "Water Temple": '✔' if inventory[66151] else '✕', + "Spirit Temple": '✔' if inventory[66152] else '✕', + "Shadow Temple": '✔' if inventory[66153] else '✕', + "Ganon's Castle": '✔' if inventory[66154] else '✕', + } + + # Victory condition + game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0) + display_data['game_finished'] = game_state == 30 + + return render_template("ootTracker.html", + inventory=inventory, player=tracked_player, team=tracked_team, room=room, player_name=player_name, + icons=oot_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, + checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, + small_key_counts=small_key_counts, boss_key_counts=boss_key_counts, + **display_data) + else: checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()) player_received_items = {}