From 8742aadc72d7f3c30bc6915646aafabe46657c5c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 25 Jun 2022 16:01:42 -0500 Subject: [PATCH] Player tracker (#710) * Player tracker: implement a stylized tracker (#447) * Move generic tracker to a WebWorld method * render both a generic tracker at generic_tracker and the specific tracker at /tracker * create a base template for generic specific tracker and instantiate some information before callng it * some baseline for the playerTracker.html. update information fed from tracker.py * playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon. * player tracker: starting work on regions table * player tracker: change method calls * Move generic tracker to a WebWorld method * render both a generic tracker at generic_tracker and the specific tracker at /tracker * create a base template for generic specific tracker and instantiate some information before callng it * some baseline for the playerTracker.html. update information fed from tracker.py * playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon. * player tracker: starting work on regions table * player tracker: change method calls * Move generic tracker to a WebWorld method * create a base template for generic specific tracker and instantiate some information before callng it * some baseline for the playerTracker.html. update information fed from tracker.py * playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon. * player tracker: starting work on regions table * player tracker: switch item, icon and location tables to flex views. Some styling based on theme * Player Tracker: Finish building html template for all blocks. Set groundwork for theme styling * Player Tracker: Implement tracker class. Document tracker usage. * Player Tracker: Add button to switch between trackers. Some styling for styled tracker. * Player Tracker: reword some text. Attempt to fix page refreshing. * Player Tracker: reremove the TODOs that got merged back in accidentally. * player tracker: move render_template import to webworld so it isn't required outside of webhost * Player Tracker: code cleanup, typing. Add inventory with names to PlayerTracker class in case custom trackers want to use it to change their prog_items attribute. * Player Tracker: delete a line I forgot about. Add typing to theme. * Player Tracker: Generate checks_done automatically so worlds don't have to do it * Player Tracker: Add typing to PlayerTracker class in webworld method. Update documentation * Player Tracker: code cleanup * Player Tracker: Sort of implement fetch (works but could be better). Make playerTracker.html more readable. * specific trackers: significant html cleanup. DOM Endpoint auto updating page every 30 seconds * Changes by Kono * specific trackers: cache and only load the data once every minute * specific tracker: allow for one icon placement to be used for multiple items. * Player tracker fixes/updates (#635) * Move generic tracker to a WebWorld method * render both a generic tracker at generic_tracker and the specific tracker at /tracker * create a base template for generic specific tracker and instantiate some information before callng it * some baseline for the playerTracker.html. update information fed from tracker.py * playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon. * player tracker: starting work on regions table * player tracker: change method calls * Move generic tracker to a WebWorld method * render both a generic tracker at generic_tracker and the specific tracker at /tracker * create a base template for generic specific tracker and instantiate some information before callng it * some baseline for the playerTracker.html. update information fed from tracker.py * playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon. * player tracker: starting work on regions table * player tracker: change method calls * Move generic tracker to a WebWorld method * create a base template for generic specific tracker and instantiate some information before callng it * some baseline for the playerTracker.html. update information fed from tracker.py * playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon. * player tracker: starting work on regions table * player tracker: switch item, icon and location tables to flex views. Some styling based on theme * Player Tracker: Finish building html template for all blocks. Set groundwork for theme styling * Player Tracker: Implement tracker class. Document tracker usage. * Player Tracker: Add button to switch between trackers. Some styling for styled tracker. * Player Tracker: reword some text. Attempt to fix page refreshing. * Player Tracker: reremove the TODOs that got merged back in accidentally. * player tracker: move render_template import to webworld so it isn't required outside of webhost * Player Tracker: code cleanup, typing. Add inventory with names to PlayerTracker class in case custom trackers want to use it to change their prog_items attribute. * Player Tracker: delete a line I forgot about. Add typing to theme. * Player Tracker: Generate checks_done automatically so worlds don't have to do it * Player Tracker: Add typing to PlayerTracker class in webworld method. Update documentation * Player Tracker: code cleanup * Player Tracker: Sort of implement fetch (works but could be better). Make playerTracker.html more readable. * specific trackers: significant html cleanup. DOM Endpoint auto updating page every 30 seconds * Changes by Kono * specific trackers: cache and only load the data once every minute * specific tracker: allow for one icon placement to be used for multiple items. * lttp: move tracker to new format. will need more modification to generic solution to handle region keys tracking. likely a new html template that inherits the current * lttp: fix broken icons rendering, add in progressive mail that i forgor. reorder some icons * tracker: fix non edited trackers being broken from changes. * tracker: move theme application before modify method so trackers can use a different theme than the world if desired. * tracker: starting work on key tracking. * tracker: styling and cleanup by Farrak * tracker: styling and cleanup by Farrak * tracker: styling and cleanup of playerTracker.html * Revert playerTracker.html * trackers: rename some files for clarity. move trackers into their own subdirectory * small tracker.py cleanup * move minecraft tracker to new system * add item link attributing from upstream * change getPlayerTracker to get_player_tracker. refactor broken linkings * refactor styling files to trackers folders * fix broken image in minecraft tracker. move oot tracker to new system * clean up my oot nightmare * rename lttpKeysTracker to zeldaKeysTracker. Move oot to keys tracker * implement zeldaKeysTracker.js. fix table locations hiding/showing --- WebHostLib/api/__init__.py | 2 +- WebHostLib/api/tracker.py | 50 ++ WebHostLib/static/assets/lttp-tracker.js | 20 - .../assets/{ => trackers}/minecraftTracker.js | 0 .../assets/{ => trackers}/ootTracker.js | 0 .../static/assets/trackers/playerTracker.js | 82 +++ .../{ => trackers}/supermetroidTracker.js | 0 .../{ => trackers}/timespinnerTracker.js | 0 .../static/assets/{ => trackers}/tracker.js | 0 .../assets/trackers/zeldaKeysTracker.js | 82 +++ .../styles/{ => trackers}/lttp-tracker.css | 0 .../styles/{ => trackers}/ootTracker.css | 0 .../static/styles/trackers/playerTracker.css | 150 +++++ .../{ => trackers}/supermetroidTracker.css | 0 .../{ => trackers}/timespinnerTracker.css | 0 .../static/styles/{ => trackers}/tracker.css | 11 + WebHostLib/templates/lttpTracker.html | 86 --- WebHostLib/templates/macros.html | 2 +- .../{ => trackers}/genericTracker.html | 7 +- .../{ => trackers}/minecraftTracker.html | 4 +- .../multiworldTracker.html} | 8 +- .../templates/{ => trackers}/ootTracker.html | 0 .../templates/trackers/playerTracker.html | 99 +++ .../{ => trackers}/supermetroidTracker.html | 4 +- .../{ => trackers}/timespinnerTracker.html | 4 +- .../templates/trackers/zeldaKeysTracker.html | 77 +++ WebHostLib/tracker.py | 570 ++++++------------ worlds/AutoWorld.py | 18 +- worlds/alttp/__init__.py | 226 +++++++ worlds/minecraft/__init__.py | 73 +++ worlds/oot/__init__.py | 129 +++- 31 files changed, 1182 insertions(+), 522 deletions(-) create mode 100644 WebHostLib/api/tracker.py delete mode 100644 WebHostLib/static/assets/lttp-tracker.js rename WebHostLib/static/assets/{ => trackers}/minecraftTracker.js (100%) rename WebHostLib/static/assets/{ => trackers}/ootTracker.js (100%) create mode 100644 WebHostLib/static/assets/trackers/playerTracker.js rename WebHostLib/static/assets/{ => trackers}/supermetroidTracker.js (100%) rename WebHostLib/static/assets/{ => trackers}/timespinnerTracker.js (100%) rename WebHostLib/static/assets/{ => trackers}/tracker.js (100%) create mode 100644 WebHostLib/static/assets/trackers/zeldaKeysTracker.js rename WebHostLib/static/styles/{ => trackers}/lttp-tracker.css (100%) rename WebHostLib/static/styles/{ => trackers}/ootTracker.css (100%) create mode 100644 WebHostLib/static/styles/trackers/playerTracker.css rename WebHostLib/static/styles/{ => trackers}/supermetroidTracker.css (100%) rename WebHostLib/static/styles/{ => trackers}/timespinnerTracker.css (100%) rename WebHostLib/static/styles/{ => trackers}/tracker.css (94%) delete mode 100644 WebHostLib/templates/lttpTracker.html rename WebHostLib/templates/{ => trackers}/genericTracker.html (90%) rename WebHostLib/templates/{ => trackers}/minecraftTracker.html (96%) rename WebHostLib/templates/{tracker.html => trackers/multiworldTracker.html} (98%) rename WebHostLib/templates/{ => trackers}/ootTracker.html (100%) create mode 100644 WebHostLib/templates/trackers/playerTracker.html rename WebHostLib/templates/{ => trackers}/supermetroidTracker.html (97%) rename WebHostLib/templates/{ => trackers}/timespinnerTracker.html (98%) create mode 100644 WebHostLib/templates/trackers/zeldaKeysTracker.html diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index c2f9b3840f..3ca15b4acf 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -46,4 +46,4 @@ def get_datapackge_versions(): return version_package -from . import generate, user # trigger registration +from . import generate, user, tracker # trigger registration diff --git a/WebHostLib/api/tracker.py b/WebHostLib/api/tracker.py new file mode 100644 index 0000000000..eff6ac2729 --- /dev/null +++ b/WebHostLib/api/tracker.py @@ -0,0 +1,50 @@ +import collections + +from flask import jsonify +from typing import Optional, Dict, Any, Tuple, List +from Utils import restricted_loads +from uuid import UUID + +from ..models import Room +from . import api_endpoints +from ..tracker import fill_tracker_data, get_static_room_data +from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name +from WebHostLib import cache + + +@api_endpoints.route('/tracker///') +@cache.memoize(timeout=60) +def update_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int): + + room: Optional[Room] = Room.get(tracker=tracker) + locations = get_static_room_data(room)[0] + items_counter: Dict[int, collections.Counter] = get_item_names_counter(locations) + player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \ + slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player) + + # convert numbers to string + for item in player_tracker.items_received: + if items_counter[tracked_player][item] == 1: + player_tracker.items_received[item] = '✔' + else: + player_tracker.items_received[item] = str(player_tracker.items_received[item]) + + return jsonify({ + "items_received": player_tracker.items_received, + "checked_locations": list(sorted(player_tracker.checked_locations)), + "icons": display_icons, + "progressive_names": player_tracker.progressive_names + }) + + +@cache.cached() +def get_item_names_counter(locations: Dict[int, Dict[int, Tuple[int, int, int]]]): + # create and fill dictionary of all progression items for players + items_counters: Dict[int, collections.Counter] = {} + for player in locations: + for location in locations[player]: + item, recipient, flags = locations[player][location] + item_name = lookup_any_item_id_to_name[item] + items_counters.setdefault(recipient, collections.Counter())[item_name] += 1 + + return items_counters diff --git a/WebHostLib/static/assets/lttp-tracker.js b/WebHostLib/static/assets/lttp-tracker.js deleted file mode 100644 index 3f01f93cd3..0000000000 --- a/WebHostLib/static/assets/lttp-tracker.js +++ /dev/null @@ -1,20 +0,0 @@ -window.addEventListener('load', () => { - 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 and location trackers - document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML; - document.getElementById('location-table').innerHTML = fakeDOM.getElementById('location-table').innerHTML; - - }; - ajax.open('GET', url); - ajax.send(); - }, 15000) -}); diff --git a/WebHostLib/static/assets/minecraftTracker.js b/WebHostLib/static/assets/trackers/minecraftTracker.js similarity index 100% rename from WebHostLib/static/assets/minecraftTracker.js rename to WebHostLib/static/assets/trackers/minecraftTracker.js diff --git a/WebHostLib/static/assets/ootTracker.js b/WebHostLib/static/assets/trackers/ootTracker.js similarity index 100% rename from WebHostLib/static/assets/ootTracker.js rename to WebHostLib/static/assets/trackers/ootTracker.js diff --git a/WebHostLib/static/assets/trackers/playerTracker.js b/WebHostLib/static/assets/trackers/playerTracker.js new file mode 100644 index 0000000000..d4522e1d78 --- /dev/null +++ b/WebHostLib/static/assets/trackers/playerTracker.js @@ -0,0 +1,82 @@ +window.addEventListener('load', () => { + // Reload tracker + const update = () => { + const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); + + const request = new Request('/api/tracker/' + room); + + fetch(request) + .then(response => response.json()) + .then(data => { + // update locations blocks + for (const location of data.checked_locations) { + document.getElementById(location).classList.add('acquired'); + } + // update totals checks done + let total_checks_ele = document.getElementById('total-checks'); + const total_checks = document.getElementsByClassName('location').length; + let checks_done = data.checked_locations.length; + total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks; + // update item and icons blocks + // update icons block + if (data.icons.length > 0) { + for (let item in data.icons) { + if (data.progressive_names.length > 0) { + for (let item_category in data.progressive_names) { + let i = 0; + for (let current_item in current_name) { + if (current_item === item) { + let doc_item = document.getElementById(item_category) + doc_item.children[0].src = data.icons[item]; + if (item in data.items_received) { + doc_item.children[0].classList.add('acquired'); + doc_item.children[1].innerText = item_category; + } + } + } + } + } else { + if (item in data.items_received) { + let current_item = document.getElementById(item); + current_item.children[0].classList.add('acquired'); + current_item.children[0].src = data.icons[item]; + current_item.children[1].innerText = item; + } + } + } + } else { + for (const item in data.items_received) { + if (document.getElementById(item)) { + let current_item = document.getElementById(item); + current_item.innerText = item + data.items_received[item]; + } + } + } + }); + } + + update() + setInterval(update, 30000); + + + // Collapsible regions section + const regions = document.getElementsByClassName('regions-column'); + for (let i = 0; i < regions.length; i++) { + let region_name = regions[i].id; + + const tab_header = document.getElementById(region_name+'-header'); + const locations = document.getElementById(region_name+'-locations'); + // toggle locations display + regions[i].addEventListener('click', function(event) { + if (tab_header.innerHTML.includes("▼")) { + locations.classList.remove('hidden'); + // change header text + tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲'); + } else { + locations.classList.add('hidden'); + // change header text + tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼'); + } + }); + } +}); diff --git a/WebHostLib/static/assets/supermetroidTracker.js b/WebHostLib/static/assets/trackers/supermetroidTracker.js similarity index 100% rename from WebHostLib/static/assets/supermetroidTracker.js rename to WebHostLib/static/assets/trackers/supermetroidTracker.js diff --git a/WebHostLib/static/assets/timespinnerTracker.js b/WebHostLib/static/assets/trackers/timespinnerTracker.js similarity index 100% rename from WebHostLib/static/assets/timespinnerTracker.js rename to WebHostLib/static/assets/trackers/timespinnerTracker.js diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/trackers/tracker.js similarity index 100% rename from WebHostLib/static/assets/tracker.js rename to WebHostLib/static/assets/trackers/tracker.js diff --git a/WebHostLib/static/assets/trackers/zeldaKeysTracker.js b/WebHostLib/static/assets/trackers/zeldaKeysTracker.js new file mode 100644 index 0000000000..d4522e1d78 --- /dev/null +++ b/WebHostLib/static/assets/trackers/zeldaKeysTracker.js @@ -0,0 +1,82 @@ +window.addEventListener('load', () => { + // Reload tracker + const update = () => { + const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); + + const request = new Request('/api/tracker/' + room); + + fetch(request) + .then(response => response.json()) + .then(data => { + // update locations blocks + for (const location of data.checked_locations) { + document.getElementById(location).classList.add('acquired'); + } + // update totals checks done + let total_checks_ele = document.getElementById('total-checks'); + const total_checks = document.getElementsByClassName('location').length; + let checks_done = data.checked_locations.length; + total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks; + // update item and icons blocks + // update icons block + if (data.icons.length > 0) { + for (let item in data.icons) { + if (data.progressive_names.length > 0) { + for (let item_category in data.progressive_names) { + let i = 0; + for (let current_item in current_name) { + if (current_item === item) { + let doc_item = document.getElementById(item_category) + doc_item.children[0].src = data.icons[item]; + if (item in data.items_received) { + doc_item.children[0].classList.add('acquired'); + doc_item.children[1].innerText = item_category; + } + } + } + } + } else { + if (item in data.items_received) { + let current_item = document.getElementById(item); + current_item.children[0].classList.add('acquired'); + current_item.children[0].src = data.icons[item]; + current_item.children[1].innerText = item; + } + } + } + } else { + for (const item in data.items_received) { + if (document.getElementById(item)) { + let current_item = document.getElementById(item); + current_item.innerText = item + data.items_received[item]; + } + } + } + }); + } + + update() + setInterval(update, 30000); + + + // Collapsible regions section + const regions = document.getElementsByClassName('regions-column'); + for (let i = 0; i < regions.length; i++) { + let region_name = regions[i].id; + + const tab_header = document.getElementById(region_name+'-header'); + const locations = document.getElementById(region_name+'-locations'); + // toggle locations display + regions[i].addEventListener('click', function(event) { + if (tab_header.innerHTML.includes("▼")) { + locations.classList.remove('hidden'); + // change header text + tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲'); + } else { + locations.classList.add('hidden'); + // change header text + tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼'); + } + }); + } +}); diff --git a/WebHostLib/static/styles/lttp-tracker.css b/WebHostLib/static/styles/trackers/lttp-tracker.css similarity index 100% rename from WebHostLib/static/styles/lttp-tracker.css rename to WebHostLib/static/styles/trackers/lttp-tracker.css diff --git a/WebHostLib/static/styles/ootTracker.css b/WebHostLib/static/styles/trackers/ootTracker.css similarity index 100% rename from WebHostLib/static/styles/ootTracker.css rename to WebHostLib/static/styles/trackers/ootTracker.css diff --git a/WebHostLib/static/styles/trackers/playerTracker.css b/WebHostLib/static/styles/trackers/playerTracker.css new file mode 100644 index 0000000000..0faa02cdef --- /dev/null +++ b/WebHostLib/static/styles/trackers/playerTracker.css @@ -0,0 +1,150 @@ +/* CSS Overrides */ +.dirt-wrapper{ + background-color: #897249; +} + +.dirt-wrapper h1{} + +.grass-wrapper{ + background-color: #3fb24a; +} + +.grass-wrapper h1{} + +.grassFlowers-wrapper{ + background-color: #3fb24a; +} + +.grassFlowers-wrapper h1{} + +.ice-wrapper{ + background-color: #afe0ef; +} + +.ice-wrapper h1{} + +.jungle-wrapper{ + background-color: #2a7808; +} + +.jungle-wrapper h1{} + +.ocean-wrapper{ + background-color: #3667b1; +} + +.ocean-wrapper h1{} + +.partyTime-wrapper{ + background-color: #3a0f69; + color: #ffffff; +} + +.partyTime-wrapper h1{} + + +/* Actual Styles */ +h1 { + font-size: 20px; + color: #ffffff; + padding: 5px; + text-align: center; + text-shadow: 1px 1px black; +} + +h2 { + padding: 8px; +} + +#player-keys-tracker{ + width: 600px; +} + +#items-container{ + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-evenly; + padding: 5px; +} + +#items-container div{ + margin: 0; + padding: 0; +} + +.image-container{ + display: absolute; + height: 75px; + width: 75px; +} + +.bottom-text{ + position: relative; + align-items: bottom; + text-align: center; +} + +.icon{ + height: 100%; + position: relative; + left: 15px; + max-width: 45px; + max-height: 45px; + filter: grayscale(100%) contrast(75%) brightness(40%); +} + +.icon.acquired{ + filter: none; +} + +.total-checks{ + text-align: center; + padding: 5px; + font-size: 18px; +} + +.locations-container{ + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 5px; + margin-left: 50px; + margin-right: 50px; +} + +.location.acquired{ + text-decoration: line-through; + filter: none; +} + +.regions-container{ + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: space-evenly; + padding: 5px; + text-align: center; +} + +.regions-header{ + font-size: 18px; + padding: 15px; + cursor: pointer; + text-align: center; +} + +.hidden{ + display: none; +} + +.button-link{ + display: block; + width: 100%; + height: 30px; + text-align: center; + text-decoration: none; + line-height: 30px; + background-color: lightgrey; + cursor: pointer; + color: inherit; +} \ No newline at end of file diff --git a/WebHostLib/static/styles/supermetroidTracker.css b/WebHostLib/static/styles/trackers/supermetroidTracker.css similarity index 100% rename from WebHostLib/static/styles/supermetroidTracker.css rename to WebHostLib/static/styles/trackers/supermetroidTracker.css diff --git a/WebHostLib/static/styles/timespinnerTracker.css b/WebHostLib/static/styles/trackers/timespinnerTracker.css similarity index 100% rename from WebHostLib/static/styles/timespinnerTracker.css rename to WebHostLib/static/styles/trackers/timespinnerTracker.css diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/trackers/tracker.css similarity index 94% rename from WebHostLib/static/styles/tracker.css rename to WebHostLib/static/styles/trackers/tracker.css index e203d9e97d..4fd0ab657c 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/trackers/tracker.css @@ -51,6 +51,17 @@ table.dataTable{ color: #000000; } +table.dataTable img.icon{ + height: 100%; + max-width: 60px; + max-height: 60px; + filter: grayscale(100%) contrast(75%) brightness(50%); +} + +table.dataTable img.acquired{ + filter: none; +} + table.dataTable thead{ font-family: LexendDeca-Regular, sans-serif; } diff --git a/WebHostLib/templates/lttpTracker.html b/WebHostLib/templates/lttpTracker.html deleted file mode 100644 index 3f1c35793e..0000000000 --- a/WebHostLib/templates/lttpTracker.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - {{ player_name }}'s Tracker - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - {% if key_locations and "Universal" not in key_locations %} - - {% endif %} - {% if big_key_locations %} - - {% endif %} - - {% for area in sp_areas %} - - - - {% if key_locations and "Universal" not in key_locations %} - - {% endif %} - {% if big_key_locations %} - - {% endif %} - - {% endfor %} -
{{ area }}{{ checks_done[area] }} / {{ checks_in_area[area] }} - {{ inventory[small_key_ids[area]] if area in key_locations else '—' }} - - {{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }} -
-
- - diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 70b41fad9e..9157424594 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -47,7 +47,7 @@ No file to download for this game. {% endif %} - Tracker + Tracker {% endfor %} diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/trackers/genericTracker.html similarity index 90% rename from WebHostLib/templates/genericTracker.html rename to WebHostLib/templates/trackers/genericTracker.html index 2a48c66f7e..f3f6a6a18c 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/trackers/genericTracker.html @@ -2,9 +2,9 @@ {% block head %} {{ super() }} {{ player_name }}'s Tracker - + - + {% endblock %} {% block body %} @@ -13,6 +13,9 @@
This tracker will automatically update itself periodically. + + Go to Styled Tracker +
diff --git a/WebHostLib/templates/minecraftTracker.html b/WebHostLib/templates/trackers/minecraftTracker.html similarity index 96% rename from WebHostLib/templates/minecraftTracker.html rename to WebHostLib/templates/trackers/minecraftTracker.html index 2f38822d72..528fab0272 100644 --- a/WebHostLib/templates/minecraftTracker.html +++ b/WebHostLib/templates/trackers/minecraftTracker.html @@ -2,8 +2,8 @@ {{ player_name }}'s Tracker - - + + diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/trackers/multiworldTracker.html similarity index 98% rename from WebHostLib/templates/tracker.html rename to WebHostLib/templates/trackers/multiworldTracker.html index 889ed2b272..abffb6628e 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/trackers/multiworldTracker.html @@ -2,9 +2,9 @@ {% block head %} {{ super() }} Multiworld Tracker - + - + {% endblock %} {% block body %} @@ -44,7 +44,7 @@ {%- for player, items in players.items() -%} - {%- if (team, loop.index) in video -%} {%- if video[(team, loop.index)][0] == "Twitch" -%} @@ -121,7 +121,7 @@ {%- for player, checks in players.items() -%} - {%- for area in ordered_areas -%} diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/trackers/ootTracker.html similarity index 100% rename from WebHostLib/templates/ootTracker.html rename to WebHostLib/templates/trackers/ootTracker.html diff --git a/WebHostLib/templates/trackers/playerTracker.html b/WebHostLib/templates/trackers/playerTracker.html new file mode 100644 index 0000000000..961cdf220b --- /dev/null +++ b/WebHostLib/templates/trackers/playerTracker.html @@ -0,0 +1,99 @@ +{% block head %} + +{{ player_name }}'s Tracker + + + +{% endblock %} + +{% block body %} + +
+ + Go to Generic Tracker + + + {% if icons %} + + {% block icons_render %} + +

Items

+
+ {%- for item in icons %} +
+ +
+ {%- endfor %} +
+ + {% endblock %} + + {% else %} + + {% block item_names_render %} +

Items

+
+ {%- for item in received_items|sort -%} +
+ {{ item }} + {% if all_progression_items[item] > 1 %} + {{ received_items[item] }} + {% else %} + ✔ + {% endif %} +
+ {%- endfor -%} +
+ + {% endblock %} + + {% endif %} + + +{# div for total checks done as percentage. Probably needs to be put somewhere else but I liked how it looked here #} +
+ Total Checks Done: {{ checked_locations|length }}/{{ locations|length }} +
+ + + {% if regions %} + + {% block regions_render %} + +
+ {% for region in regions %} +
+

{{ region }} ▼ {{ checks_done[region]|length }} / {{ regions[region]|length }}

+ +
+ {% endfor %} +
+ + {% endblock %} + + {% else %} + + {% block locations_render %} + +

Locations

+
+ {% for location in locations %} +
+ {{ location }} +
+ {% endfor %} +
+ + {% endblock %} + + {% endif %} +
+ +{% endblock %} diff --git a/WebHostLib/templates/supermetroidTracker.html b/WebHostLib/templates/trackers/supermetroidTracker.html similarity index 97% rename from WebHostLib/templates/supermetroidTracker.html rename to WebHostLib/templates/trackers/supermetroidTracker.html index 342f75642f..ce3f2ecccd 100644 --- a/WebHostLib/templates/supermetroidTracker.html +++ b/WebHostLib/templates/trackers/supermetroidTracker.html @@ -2,8 +2,8 @@ {{ player_name }}'s Tracker - - + + diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/trackers/timespinnerTracker.html similarity index 98% rename from WebHostLib/templates/timespinnerTracker.html rename to WebHostLib/templates/trackers/timespinnerTracker.html index bd589e1068..c75a32b09e 100644 --- a/WebHostLib/templates/timespinnerTracker.html +++ b/WebHostLib/templates/trackers/timespinnerTracker.html @@ -2,8 +2,8 @@ {{ player_name }}'s Tracker - - + + diff --git a/WebHostLib/templates/trackers/zeldaKeysTracker.html b/WebHostLib/templates/trackers/zeldaKeysTracker.html new file mode 100644 index 0000000000..cd6fdab967 --- /dev/null +++ b/WebHostLib/templates/trackers/zeldaKeysTracker.html @@ -0,0 +1,77 @@ +{% block head %} + + {{ player_name }}'s Keys Tracker + + + +{% endblock %} + +{# this tracker is mostly similar to the generic player tracker but +also adds a table with the key and checks counts for each region in the middle #} + +{% block body %} + +
+ + Go to Generic Tracker + +

Items

+
+ {% for item in icons %} + {% if item not in ['Small Key', 'Big Key'] %} +
+ +
+ {% endif %} + {% endfor %} +
+ +
+ Total Checks Done: {{ checked_locations|length }}/{{ locations|length }} +
+ +
{{ loop.index }}
{{ loop.index }} {{ player_names[(team, loop.index)]|e }}
+ + + + + + {% for region in regions %} + + + {% if region in region_keys %} + {%- if region_keys[region]|length > 1 %} + + + {%- else %} + {% if 'Small Key' in region_keys[region][0] %} + + + {% else %} + + + {% endif %} + {%- endif%} + {% else %} + + + {% endif %} + + + + {% for location in regions[region] %} + + + + {% endfor %} + + {% endfor %} + + + +{% endblock %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5e249c19ea..26b9b52f20 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1,6 +1,6 @@ import collections import typing -from typing import Counter, Optional, Dict, Any, Tuple +from typing import Counter, Optional, Dict, Any, Tuple, Set, List, TYPE_CHECKING from flask import render_template from werkzeug.exceptions import abort @@ -11,9 +11,53 @@ from worlds.alttp import Items from WebHostLib import app, cache, Room from Utils import restricted_loads from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name +from worlds.AutoWorld import AutoWorldRegister from MultiServer import get_item_name_from_id, Context from NetUtils import SlotType + +class PlayerTracker: + """This class will create a basic 'prettier' tracker for each world using their themes automatically. This + can be overridden to customize how it will appear. Can provide icons and custom regions. The html used is also + a jinja template that can be overridden if you want your tracker to look different in certain aspects. To render + icons and regions add dictionaries to the relevant attributes of the tracker_info. To customize the layout of + your icons you can create a new html in your world and extend playerTracker.html and overwrite the icons_render + block then change the tracker_info template attribute to your template.""" + + template: str = 'playerTracker.html' + icons: Dict[str, str] = {} + progressive_items: List[str] = [] + progressive_names: Dict[str, List[str]] = {} + regions: Dict[str, List[str]] = {} + checks_done: Dict[str, Set[str]] = {} + room: Any + team: int + player: int + name: str + all_locations: Set[str] + checked_locations: Set[str] + all_prog_items: Counter[str] + items_received: Counter[str] + received_prog_items: Counter[str] + slot_data: Dict[any, any] + theme: str + + region_keys: Dict[str, str] = {} + + def __init__(self, room: Any, team: int, player: int, name: str, all_locations: Set[str], + checked_locations: set, all_progression_items: Counter[str], items_received: Counter[str], + slot_data: Dict[any, any]): + self.room = room + self.team = team + self.player = player + self.name = name + self.all_locations = all_locations + self.checked_locations = checked_locations + self.all_prog_items = all_progression_items + self.items_received = items_received + self.slot_data = slot_data + + alttp_icons = { "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", "Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", @@ -288,7 +332,7 @@ def get_static_room_data(room: Room): @app.route('/tracker///') @cache.memoize(timeout=60) # multisave is currently created at most every minute -def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): # Team and player must be positive and greater than zero if tracked_team < 0 or tracked_player < 1: abort(404) @@ -297,16 +341,81 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want if not room: abort(404) - # Collect seed information and pare it down to a single player + player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \ + slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player) + + game_name = games[tracked_player] + # TODO move all games in game_specific_trackers to new system + if game_name in game_specific_trackers and not want_generic: + specific_tracker = game_specific_trackers.get(game_name, None) + return specific_tracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player, player_name, + seed_checks_in_area, lttp_checks_done, slot_data[tracked_player]) + elif game_name in AutoWorldRegister.world_types and not want_generic: + return render_template( + "trackers/" + player_tracker.template, + all_progression_items=player_tracker.all_prog_items, + player=player_tracker.player, + team=player_tracker.team, + room=player_tracker.room, + player_name=player_tracker.name, + checked_locations=sorted(player_tracker.checked_locations), + locations=sorted(player_tracker.all_locations), + theme=player_tracker.theme, + icons=display_icons, + regions=player_tracker.regions, + checks_done=player_tracker.checks_done, + region_keys=player_tracker.region_keys + ) + else: + return __renderGenericTracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player, player_name, seed_checks_in_area, lttp_checks_done) + + +@app.route('/generic_tracker///') +@cache.memoize(timeout=60) +def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int): + return get_player_tracker(tracker, tracked_team, tracked_player, True) + + +def get_tracker_icons_and_regions(player_tracker: PlayerTracker) -> Dict[str, str]: + """this function allows multiple icons to be used for the same item but it does require the world to submit both + a progressive_items list and the icons dict together""" + display_icons: Dict[str, str] = {} + if player_tracker.progressive_names and player_tracker.icons: + for item in player_tracker.progressive_items: + if item in player_tracker.progressive_names: + level = min(player_tracker.items_received[item], len(player_tracker.progressive_names[item]) - 1) + display_name = player_tracker.progressive_names[item][level] + if display_name in player_tracker.icons: + display_icons[item] = player_tracker.icons[display_name] + else: + display_icons[item] = player_tracker.icons[item] + else: + display_icons[item] = player_tracker.icons[item] + else: + if player_tracker.progressive_items and player_tracker.icons: + for item in player_tracker.progressive_items: + display_icons[item] = player_tracker.icons[item] + + if player_tracker.regions: + for region in player_tracker.regions: + for location in region: + if location in player_tracker.checked_locations: + player_tracker.checks_done.setdefault(region, set()).add(location) + + return display_icons + + +def fill_tracker_data(room: Room, team: int, player: int) -> Tuple: + """Collect seed information and pare it down to a single player""" locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ precollected_items, games, slot_data, groups = get_static_room_data(room) - player_name = names[tracked_team][tracked_player - 1] - location_to_area = player_location_to_area[tracked_player] + player_name = names[team][player - 1] + location_to_area = player_location_to_area[player] inventory = collections.Counter() - checks_done = {loc_name: 0 for loc_name in default_locations} + lttp_checks_done = {loc_name: 0 for loc_name in default_locations} # Add starting items to inventory - starting_items = precollected_items[tracked_player] + starting_items = precollected_items[player] if starting_items: for item_id in starting_items: attribute_item_solo(inventory, item_id) @@ -316,399 +425,69 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want else: multisave: Dict[str, Any] = {} - slots_aimed_at_player = {tracked_player} + slots_aimed_at_player = {player} for group_id, group_members in groups.items(): - if tracked_player in group_members: + if player in group_members: slots_aimed_at_player.add(group_id) + checked_locations = set() # Add items to player inventory for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items(): # Skip teams and players not matching the request player_locations = locations[ms_player] - if ms_team == tracked_team: + if ms_team == team: # If the player does not have the item, do nothing for location in locations_checked: if location in player_locations: item, recipient, flags = player_locations[location] if recipient in slots_aimed_at_player: # a check done for the tracked player attribute_item_solo(inventory, item) - if ms_player == tracked_player: # a check done by the tracked player - checks_done[location_to_area[location]] += 1 - checks_done["Total"] += 1 - specific_tracker = game_specific_trackers.get(games[tracked_player], None) - if specific_tracker and not want_generic: - return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done, slot_data[tracked_player]) - else: - return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done) + + if ms_player == player: # a check done by the tracked player + lttp_checks_done[location_to_area[location]] += 1 + lttp_checks_done["Total"] += 1 + checked_locations.add(lookup_any_location_id_to_name[location]) + + prog_items = collections.Counter + all_location_names = set() + + all_location_names = {lookup_any_location_id_to_name[id] for id in locations[player]} + prog_items = collections.Counter() + for player in locations: + for location in locations[player]: + item, recipient, flags = locations[player][location] + if recipient == player: + if flags & 1: + item_name = lookup_any_item_id_to_name[item] + prog_items[item_name] += 1 + + items_received = collections.Counter() + for id in inventory: + items_received[lookup_any_item_id_to_name[id]] = inventory[id] + + player_tracker = PlayerTracker( + room, + team, + player, + player_name, + all_location_names, + checked_locations, + prog_items, + items_received, + slot_data[player] + ) + + # grab webworld and apply its theme to the tracker + webworld = AutoWorldRegister.world_types[games[player]].web + player_tracker.theme = webworld.theme + # allow the world to add information to the tracker class + webworld.modify_tracker(player_tracker) + display_icons = get_tracker_icons_and_regions(player_tracker) + + return player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, slot_data, games, player_name, display_icons -@app.route('/generic_tracker///') -def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int): - return getPlayerTracker(tracker, tracked_team, tracked_player, True) - - -def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, player_name: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: - - # Note the presence of the triforce item - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - if game_state == 30: - inventory[106] = 1 # Triforce - - # Progressive items need special handling for icons and class - progressive_items = { - "Progressive Sword": 94, - "Progressive Glove": 97, - "Progressive Bow": 100, - "Progressive Mail": 96, - "Progressive Shield": 95, - } - progressive_names = { - "Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], - "Progressive Glove": [None, 'Power Glove', 'Titan Mitts'], - "Progressive Bow": [None, "Bow", "Silver Bow"], - "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"], - "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"] - } - - # Determine which icon to use - display_data = {} - 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] - acquired = True - if not display_name: - acquired = False - display_name = progressive_names[item_name][level + 1] - base_name = item_name.split(maxsplit=1)[1].lower() - display_data[base_name + "_acquired"] = acquired - display_data[base_name + "_url"] = alttp_icons[display_name] - - # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? - sp_areas = ordered_areas[2:15] - - player_big_key_locations = set() - player_small_key_locations = set() - for loc_data in locations.values(): - for values in loc_data.values(): - item_id, item_player, flags = values - if item_player == player: - if item_id in ids_big_key: - player_big_key_locations.add(ids_big_key[item_id]) - elif item_id in ids_small_key: - player_small_key_locations.add(ids_small_key[item_id]) - - return render_template("lttpTracker.html", inventory=inventory, - player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done, - checks_in_area=seed_checks_in_area[player], - acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, - small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas, - key_locations=player_small_key_locations, - big_key_locations=player_big_key_locations, - **display_data) - - -def __renderMinecraftTracker(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) -> str: - - icons = { - "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", - "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png", - "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png", - "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png", - "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png", - "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png", - "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png", - "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png", - "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png", - "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png", - "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png", - "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png", - "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png", - "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png", - "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png", - "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", - "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", - "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", - "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", - "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", - "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", - "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", - "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", - "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", - "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", - "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", - } - - minecraft_location_ids = { - "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, - 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], - "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, - 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014], - "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046], - "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42035, 42016, 42020, - 42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42099, 42100], - "Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, 42036, - 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095], - "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091], - } - - display_data = {} - - # Determine display for progressive items - progressive_items = { - "Progressive Tools": 45013, - "Progressive Weapons": 45012, - "Progressive Armor": 45014, - "Progressive Resource Crafting": 45001 - } - progressive_names = { - "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"], - "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"], - "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"], - "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"] - } - 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] - base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_') - display_data[base_name + "_url"] = icons[display_name] - - # Multi-items - multi_items = { - "3 Ender Pearls": 45029, - "8 Netherite Scrap": 45015 - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - if count >= 0: - display_data[base_name + "_count"] = count - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in minecraft_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in minecraft_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("minecraftTracker.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, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - - -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, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: - - 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"] = 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'] = icons['Rutos Letter'] if inventory[66021] > 0 else 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 Ground": (67597, 67635), - "Thieves' Hideout": (67259, 67263), - "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 not in ["Overworld", "Thieves' Hideout"]: - # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC - return full_name[len(area):] - return full_name - - checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[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[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[player]]) - for area, (min_id, max_id) in area_id_ranges.items()} - - # Remove Thieves' Hideout checks from Overworld, since it's in the middle of the range - checks_in_area["Overworld"] -= checks_in_area["Thieves' Hideout"] - checks_done["Overworld"] -= checks_done["Thieves' Hideout"] - for loc in location_info["Thieves' Hideout"]: - del location_info["Overworld"][loc] - - 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((team, player), set()).difference(set(locations[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 Ground": inventory[66181], - "Thieves' Hideout": inventory[66182], - "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((team, player), 0) - display_data['game_finished'] = game_state == 30 - - return render_template("ootTracker.html", - inventory=inventory, player=player, team=team, room=room, player_name=playerName, - icons=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) - - -def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], +def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: set, 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[str, Any]) -> str: @@ -808,13 +587,13 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name} options = {k for k, v in slot_data.items() if v} - return render_template("timespinnerTracker.html", + return render_template("trackers/" + "timespinnerTracker.html", inventory=inventory, icons=icons, acquired_items=acquired_items, player=player, team=team, room=room, player_name=playerName, checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, options=options, **display_data) -def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], +def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: set, 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) -> str: @@ -889,6 +668,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations for item_name, item_id in multi_items.items(): base_name = item_name.split()[0].lower() + count = inventory[item_id] display_data[base_name+"_count"] = inventory[item_id] # Victory condition @@ -906,7 +686,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} checks_in_area['Total'] = sum(checks_in_area.values()) - return render_template("supermetroidTracker.html", + return render_template("trackers/" + "supermetroidTracker.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}, @@ -914,7 +694,8 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) -def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], + +def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: set, inventory: Counter, team: int, player: int, playerName: str, seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str: @@ -929,11 +710,11 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic for order_index, networkItem in enumerate(ordered_items, start=1): player_received_items[networkItem.item] = order_index - return render_template("genericTracker.html", + return render_template("trackers/" + "genericTracker.html", inventory=inventory, player=player, team=team, room=room, player_name=playerName, checked_locations=checked_locations, - not_checked_locations=set(locations[player]) - checked_locations, + not_checked_locations=locations - checked_locations, received_items=player_received_items) @@ -975,9 +756,9 @@ def getTracker(tracker: UUID): continue item, recipient, flags = player_locations[location] - if recipient in names: attribute_item(inventory, team, recipient, item) + checks_done[team][player][player_location_to_area[player][location]] += 1 checks_done[team][player]["Total"] += 1 @@ -1021,7 +802,7 @@ def getTracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, + return render_template("trackers/" + "multiworldTracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, @@ -1032,9 +813,6 @@ def getTracker(tracker: UUID): game_specific_trackers: typing.Dict[str, typing.Callable] = { - "Minecraft": __renderMinecraftTracker, - "Ocarina of Time": __renderOoTTracker, "Timespinner": __renderTimespinnerTracker, - "A Link to the Past": __renderAlttpTracker, "Super Metroid": __renderSuperMetroidTracker } \ No newline at end of file diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5cc7d62590..e28a48b24a 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,8 +1,7 @@ from __future__ import annotations import logging -import sys -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple +from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial from Options import Option @@ -42,7 +41,6 @@ class AutoWorldRegister(type): new_class = super().__new__(mcs, name, bases, dct) if "game" in dct: AutoWorldRegister.world_types[dct["game"]] = new_class - new_class.__file__ = sys.modules[new_class.__module__].__file__ return new_class @@ -100,12 +98,22 @@ class WebWorld: tutorials: List[Tutorial] # Choose a theme for your /game/* pages - # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone - theme = "grass" + # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime + theme: str = "grass" # display a link to a bug report page, most likely a link to a GitHub issue page. bug_report_page: Optional[str] + if TYPE_CHECKING: + from WebHostLib.tracker import PlayerTracker + else: + PlayerTracker = object + + def modify_tracker(self, tracker: PlayerTracker): + """Can use this to modify tracker data and add icons and regions dictionaries to + allow them to render on the game's tracker page.""" + pass + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8e4ec1c143..b33499d6d1 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -101,6 +101,232 @@ class ALTTPWeb(WebWorld): tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando] + if typing.TYPE_CHECKING: + from WebHostLib.tracker import PlayerTracker + else: + PlayerTracker = object + + def modify_tracker(self, tracker: PlayerTracker): + tracker.template = 'zeldaKeysTracker.html' + + tracker.icons = { + "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", + "Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", + "Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", + "Fighter Sword": r"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": r"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": r"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": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", + "Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", + "Silver Bow": r"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": r"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": r"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": r"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": r"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": r"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": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", + "Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", + "Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Flippers": r"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": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", + "Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", + "Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", + "Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", + "Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", + "Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", + "Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec", + "Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", + "Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", + "Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", + "Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", + "Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", + "Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", + "Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", + "Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", + "Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", + "Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", + "Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", + "Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", + "Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", + "Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", + "Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", + "Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", + "Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", + "Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", + "Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", + "Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", + "Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", + "Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", + "Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", + "Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", + "Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", + "Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", + "Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", + "Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", + "Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", + "Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png", + "Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", + "Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", + "Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", + "Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", + "Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74" + } + + tracker.regions = { + 'Light World': [ + 'Lost Woods Hideout', 'Lumberjack Tree', 'Mushroom', 'Master Sword Pedestal', 'Bottle Merchant', 'Flute Spot', + 'Blind\'s Hideout - Top', 'Blind\'s Hideout - Left', 'Blind\'s Hideout - Right', 'Blind\'s Hideout - Far Left', 'Blind\'s Hideout - Far Right', + 'Link\'s House', 'Link\'s Uncle', 'Secret Passage', + 'King Zora', 'Zora\'s Ledge', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', + 'King\'s Tomb', 'Graveyard Cave', 'Bonk Rock Cave', + 'Sunken Treasure', 'Floodgate Chest', 'Hobo', 'Ice Rod Cave', 'Lake Hylia Island', + 'Kakariko Tavern', 'Chicken House', 'Sick Kid', + 'Blacksmith', 'Purple Chest', 'Magic Bat', + 'Aginah\'s Cave', 'Cave 45', 'Checkerboard Cave', + 'Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla', + 'Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom', + 'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy', + 'Library', 'Maze Race', 'Potion Shop', 'Desert Ledge', + 'Old Man', 'Spectacle Rock', + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', + 'Spiral Cave', 'Ether Tablet' + ], + 'Dark World': [ + 'Pyramid', 'Catfish', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', + 'Stumpy', 'Digging Game', + 'Bombos Tablet', + 'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', + 'Peg Cave', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Bumper Cave Ledge', + 'Mire Shed - Left', 'Mire Shed - Right', + 'Superbunny Cave - Top', 'Superbunny Cave - Bottom', + 'Spike Cave', 'Floating Island', 'Mimic Cave', + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', + + ], + 'Desert Palace': [ + 'Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest', + 'Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest', + 'Desert Palace - Boss' + ], + 'Eastern Palace': [ + 'Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', + 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss' + ], + 'Hyrule Castle': [ + 'Hyrule Castle - Map Chest', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Zelda\'s Chest', + 'Sewers - Dark Cross', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right', + 'Sanctuary' + ], + 'Agahnims Tower': [ + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze' + ], + 'Tower of Hera': [ + 'Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest', 'Tower of Hera - Big Key Chest', + 'Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss' + ], + 'Swamp Palace': [ + 'Swamp Palace - Entrance', 'Swamp Palace - Map Chest', + 'Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', + 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest', + 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss' + ], + 'Thieves Town': [ + 'Thieves\' Town - Big Key Chest', 'Thieves\' Town - Map Chest', 'Thieves\' Town - Compass Chest', 'Thieves\' Town - Ambush Chest', + 'Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss' + ], + 'Skull Woods': [ + 'Skull Woods - Map Chest', 'Skull Woods - Pinball Room', + 'Skull Woods - Compass Chest', 'Skull Woods - Pot Prison', + 'Skull Woods - Big Chest', + 'Skull Woods - Big Key Chest', + 'Skull Woods - Bridge Room', 'Skull Woods - Boss' + ], + 'Ice Palace': [ + 'Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room', + 'Ice Palace - Spike Room', 'Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Boss' + ], + 'Misery Mire': [ + 'Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest', + 'Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest', 'Misery Mire - Boss' + ], + 'Turtle Rock': [ + 'Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Big Key Chest', 'Turtle Rock - Big Chest', + 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right', + 'Turtle Rock - Boss' + ], + 'Palace of Darkness': [ + 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - Big Key Chest', + 'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest', + 'Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest', + 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Boss' + ], + '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 - 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', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Bob\'s Chest', + 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest', + ] + } + + tracker.progressive_items = [ + 'Progressive Sword', + 'Progressive Shield', + 'Progressive Mail', + 'Progressive Bow', + 'Progressive Boomerang', + 'Hookshot', + 'Magic Powder', + 'Mushroom', + 'Bottle', + 'Lamp', + 'Progressive Glove', + 'Flippers', + 'Moon Pearl', + 'Bombos', + 'Ether', + 'Quake', + 'Fire Rod', + 'Ice Rod', + 'Hammer', + 'Book of Mudora', + 'Shovel', + 'Flute', + 'Bug Catching Net', + 'Cane of Somaria', + 'Cane of Byrna', + 'Cape', + 'Magic Mirror', + 'Small Key', + 'Big Key' + ] + + tracker.progressive_names = { + 'Progressive Bow': ['Bow', 'Silver Arrows', 'Silver Bow', 'Progressive Bow (Alt)'], + 'Bottle': ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)'], + 'Progressive Sword': ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], + 'Progressive Glove': ['Power Glove', 'Titans Mitts'], + 'Progressive Shield': ['Blue Shield', 'Red Shield', 'Mirror Shield'], + 'Progressive Boomerang': ['Red Boomerang', 'Blue Boomerang'], + 'Progressive Mail': ['Green Mail', 'Blue Mail', 'Red Mail'], + 'Small Key': [f'Small Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}], + 'Big Key': [f'Big Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}], + } + + tracker.region_keys = { + region: [f'Small Key ({region})', f'Big Key ({region})'] for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'} + } + class ALTTPWorld(World): """ diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index e5dbe0b0cd..8da3eb1a26 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -49,6 +49,79 @@ class MinecraftWebWorld(WebWorld): tutorials = [setup, setup_es, setup_sv] + def modify_tracker(self, tracker): + tracker.icons = { + "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", + "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png", + "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png", + "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png", + "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png", + "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png", + "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png", + "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png", + "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png", + "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png", + "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png", + "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png", + "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png", + "Brewing": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png", + "Ender Pearls": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png", + "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", + "Archery": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", + "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", + "Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", + "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", + "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", + "Enchanting": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", + "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", + "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", + "Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", + "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", + } + + tracker.progressive_items = [ + "Progressive Tools", "Progressive Weapons", "Progressive Armor", "Progressive Resource Crafting", + "Brewing", "Ender Pearls", "Bucket", "Archery", "Shield", "Bed", "Bottle", "Netherite Scrap", + "Flint and Steel", "Enchanting", "Fishing Rod", "Campfire", "Spyglass" + ] + + tracker.progressive_names = { + "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"], + "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"], + "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"], + "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"] + } + + tracker.regions = { + "Story": ["Minecraft", "Stone Age", "Getting an Upgrade", "Acquire Hardware", "Suit Up", + "Not Today, Thank You", "Isn't It Iron Pick", "Diamonds!", "Cover Me With Diamonds", "Enchanter", + "Hot Stuff", "Ice Bucket Challenge", "We Need to Go Deeper", "Zombie Doctor", "Eye Spy", "The End?"], + + "Nether": ["Nether", "Return to Sender", "Uneasy Alliance", "Those Were the Days", "War Pigs", + "Hidden in the Depths", "Country Lode, Take Me Home", "Cover Me in Debris", "Subspace Bubble", + "A Terrible Fortress", "Spooky Scary SKeleton", "This Boat Has Legs", "Hot Tourist Destinations"], + + "The End": ["The End", "Free the End", "The Next Generation", "Remote Getaway", + "The City at the End of the Game", "Sky's the Limit", "Great View From Up Here", + "The End... Again...", "You Need a Mint"], + + "Adventure": ["Adventure", "Voluntary Exile", "Is It a Bird?", "Is It a Balloon?", "Is It a Plane?", + "Hero of the Village", "Monster Hunter", "A Throwaway Joke", "Very Very Frightening", + "Take Aim", "Sniper Duel", "Bullseye", "Monsters Hunted", "Postmortal", "What a Deal!", + "Hired Help", "Sticky Situation", "Ol' Betsy", "Two Birds, One Arrow", + "Who's the Pillager Now?", "Arbalistic", "Sweet Dreams", "Adventuring Time", "Surge Protector", + "Light as a Rabbit"], + + "Husbandry": ["Husbandry", "Bee Our Guest", "The Parrots and the Bats", "Two by Two", "Best Friends Forever", + "A Complete Catalogue", "Fishy Business", "Tactical Fishing", "Total Beelocation", + "A Seedy Place", "A Balanced Diet", "Serious Dedication", "Whatever Floats Your Goat!", + "Glow and Behold!", "Wax On", "Wax Off", "The Cutest Predator", + "The Healing Power of Friendship"], + + "Archipelago": ["Getting Wood", "Time to Mine!", "Hot Topic", "Bake Bread", "The Lie", "On a Rail", + "Time to Strike!", "Cow Tipper", "When Pigs Fly", "Overkill", "Librarian", "Overpowered"] + } + class MinecraftWorld(World): """ diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b640578c16..5ce6b069d3 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -9,7 +9,7 @@ from .Location import OOTLocation, LocationFactory, location_name_to_id from .Entrance import OOTEntrance from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError from .Items import OOTItem, item_table, oot_data_to_ap_id -from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool +from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool, normal_bottles from .Regions import OOTRegion, TimeOfDay from .Rules import set_rules, set_shop_rules, set_entrances_based_rules from .RuleParser import Rule_AST_Transformer @@ -87,6 +87,133 @@ class OOTWeb(WebWorld): tutorials = [setup, setup_es] + def modify_tracker(self, tracker): + tracker.template = 'zeldaKeysTracker.html' + tracker.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", + "Din's Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png", + "Farore's Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png", + "Nayru's 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 Tokens": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png", + "Triforce Pieces": "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", + "Zelda's Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Epona's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Saria's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Sun's 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", + "Big Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png", + } + + tracker.progressive_items = [ + "Ocarina", "Bombs", "Bow", "Fire Arrows", "Kokiri Sword", "Biggoron Sword", "Mirror Shield", + "Slingshot", "Bombchus", "Progressive Hookshot", "Ice Arrows", "Progressive Strength", "Goron Tunic", "Zora Tunic", + "Boomerang", "Lens of Truth", "Megaton Hammer", "Light Arrows", "Progressive Scale", "Iron Boots", "Hover Boots", + "Bottles", "Din's Fire", "Farore's Wind", "Nayru's Love", "Progressive Wallet", "Magic Meter", "Gerudo Membership Card", + "Zelda's Lullaby", "Epona's Song", "Saria's Song", "Sun's Song", "Song of Time", "Song of Storms", "Gold Skulltula Tokens", + "Minuet of Forest", "Bolero of Fire", "Serenade of Water", "Requiem of Spirit", "Nocturne of Shadow", "Prelude of Light", "Triforce Pieces", + "Small Key", "Big Key" + ] + + tracker.progressive_names = { + "Progressive Hookshot": ["Hookshot", "Longshot"], + "Progressive Strength": ["Goron Bracelet", "Golden Gauntlets"], + "Progressive Wallet": ["Adults Wallet", "Giants Wallet"], + "Progressive Scale": ["Silver Scale", "Gold Scale"], + "Magic Meter": ["Small Magic", "Large Magic"], + "Ocarina": ["Fairy Ocarina", "Ocarina of Time"], + "Bottles": normal_bottles + ["Rutos Letter"] + } + + location_id_to_name = {} + for name, id in location_name_to_id.items(): + location_id_to_name[id] = name + tracker.regions = {} + for id in location_id_to_name.keys(): + if id in location_name_to_id.values() and location_id_to_name[id] in tracker.all_locations: + if id < 67259: + tracker.regions.setdefault("Overworld", []).append(location_id_to_name[id]) + elif id < 67264: + tracker.regions.setdefault("Thieves' Hideout", []).append(location_id_to_name[id]) + elif id < 67281: + tracker.regions.setdefault("Overworld", []).append(location_id_to_name[id]) + elif id < 67304: + tracker.regions.setdefault("Deku Tree", []).append(location_id_to_name[id]) + elif id < 67335: + tracker.regions.setdefault("Dodongo's Cavern", []).append(location_id_to_name[id]) + elif id < 67360: + tracker.regions.setdefault("Jabu Jabu's Belly", []).append(location_id_to_name[id]) + elif id < 67385: + tracker.regions.setdefault("Bottom of the Well", []).append(location_id_to_name[id]) + elif id < 67421: + tracker.regions.setdefault("Forest Temple", []).append(location_id_to_name[id]) + elif id < 67458: + tracker.regions.setdefault("Fire Temple", []).append(location_id_to_name[id]) + elif id < 67485: + tracker.regions.setdefault("Water Temple", []).append(location_id_to_name[id]) + elif id < 67533: + tracker.regions.setdefault("Shadow Temple", []).append(location_id_to_name[id]) + elif id < 67583: + tracker.regions.setdefault("Spirit Temple", []).append(location_id_to_name[id]) + elif id < 67597: + tracker.regions.setdefault("Ice Cavern", []).append(location_id_to_name[id]) + elif id < 67636: + tracker.regions.setdefault("Gerudo Training Ground", []).append(location_id_to_name[id]) + elif id < 67674: + tracker.regions.setdefault("Ganon's Castle", []).append(location_id_to_name[id]) + + tracker.region_keys = { + 'Forest Temple': ['Small Key (Forest Temple)', 'Boss Key (Forest Temple)'], + 'Fire Temple': ['Small Key (Fire Temple)', 'Boss Key (Forest Temple)'], + 'Water Temple': ['Small Key (Water Temple)', 'Boss Key (Water Temple)'], + 'Spirit Temple': ['Small Key (Spirit Temple)', 'Boss Key (Spirit Temple)'], + 'Shadow Temple': ['Small Key (Shadow Temple)', 'Boss Key (Shadow Temple)'], + 'Bottom of the Well': ['Small Key (Bottom of the Well)', 'Boss Key (Bottom of the Well)'], + 'Gerudo Training Ground': ['Small Key (Gerudo Training Ground)', 'Boss Key (Gerudo Training Ground)'], + 'Thieves Hideout': ['Small Key (Thieves Hideout)', 'Boss Key (Thieves Hideout)'], + 'Ganons Castle': ['Small Key (Ganons Castle)', 'Boss Key (Ganons Castle)'] + } + class OOTWorld(World): """
Total
{{ region }} ▼{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}{{ received_items[region_keys[region][1]] if region_keys[region][1] in received_items else '-' }}{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}--{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}--{{ checks_done[region]|length }} / {{ regions[region]|length }}