diff --git a/WebHostLib/static/assets/timespinnerTracker.js b/WebHostLib/static/assets/timespinnerTracker.js
new file mode 100644
index 00000000..a698214b
--- /dev/null
+++ b/WebHostLib/static/assets/timespinnerTracker.js
@@ -0,0 +1,49 @@
+window.addEventListener('load', () => {
+ // Reload tracker every 15 seconds
+ const url = window.location;
+ setInterval(() => {
+ const ajax = new XMLHttpRequest();
+ ajax.onreadystatechange = () => {
+ if (ajax.readyState !== 4) { return; }
+
+ // Create a fake DOM using the returned HTML
+ const domParser = new DOMParser();
+ const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
+
+ // Update item tracker
+ document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
+ // Update only counters in the location-table
+ let counters = document.getElementsByClassName('counter');
+ const fakeCounters = fakeDOM.getElementsByClassName('counter');
+ for (let i = 0; i < counters.length; i++) {
+ counters[i].innerHTML = fakeCounters[i].innerHTML;
+ }
+ };
+ ajax.open('GET', url);
+ ajax.send();
+ }, 15000)
+
+ // Collapsible advancement sections
+ const categories = document.getElementsByClassName("location-category");
+ for (let i = 0; i < categories.length; i++) {
+ let hide_id = categories[i].id.split('-')[0];
+ if (hide_id == 'Total') {
+ continue;
+ }
+ categories[i].addEventListener('click', function() {
+ // Toggle the advancement list
+ document.getElementById(hide_id).classList.toggle("hide");
+ // Change text of the header
+ const tab_header = document.getElementById(hide_id+'-header').children[0];
+ const orig_text = tab_header.innerHTML;
+ let new_text;
+ if (orig_text.includes("▼")) {
+ new_text = orig_text.replace("▼", "▲");
+ }
+ else {
+ new_text = orig_text.replace("▲", "▼");
+ }
+ tab_header.innerHTML = new_text;
+ });
+ }
+});
diff --git a/WebHostLib/static/styles/timespinnerTracker.css b/WebHostLib/static/styles/timespinnerTracker.css
new file mode 100644
index 00000000..a0012d04
--- /dev/null
+++ b/WebHostLib/static/styles/timespinnerTracker.css
@@ -0,0 +1,101 @@
+#player-tracker-wrapper{
+ margin: 0;
+}
+
+#inventory-table{
+ border-top: 2px solid #000000;
+ border-left: 2px solid #000000;
+ border-right: 2px solid #000000;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ padding: 3px 3px 10px;
+ width: 384px;
+ background-color: #8d60a7;
+}
+
+#inventory-table td{
+ width: 40px;
+ height: 40px;
+ text-align: center;
+ vertical-align: middle;
+}
+
+#inventory-table img{
+ height: 100%;
+ max-width: 40px;
+ max-height: 40px;
+ filter: grayscale(100%) contrast(75%) brightness(30%);
+}
+
+#inventory-table img.acquired{
+ filter: none;
+}
+
+#inventory-table div.counted-item {
+ position: relative;
+}
+
+#inventory-table div.item-count {
+ position: absolute;
+ color: white;
+ font-family: "Minecraftia", monospace;
+ font-weight: bold;
+ bottom: 0px;
+ right: 0px;
+}
+
+#location-table{
+ width: 384px;
+ border-left: 2px solid #000000;
+ border-right: 2px solid #000000;
+ border-bottom: 2px solid #000000;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ background-color: #8d60a7;
+ padding: 0 3px 3px;
+ font-size: 14px;
+ cursor: default;
+}
+
+#location-table th{
+ vertical-align: middle;
+ text-align: left;
+ padding-right: 10px;
+}
+
+#location-table td{
+ padding-top: 2px;
+ padding-bottom: 2px;
+ line-height: 20px;
+}
+
+#location-table td.counter {
+ text-align: right;
+ font-size: 14px;
+}
+
+#location-table td.toggle-arrow {
+ text-align: right;
+}
+
+#location-table tr#Total-header {
+ font-weight: bold;
+}
+
+#location-table img{
+ height: 100%;
+ max-width: 30px;
+ max-height: 30px;
+}
+
+#location-table tbody.locations {
+ font-size: 12px;
+}
+
+#location-table td.location-name {
+ padding-left: 16px;
+}
+
+.hide {
+ display: none;
+}
diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/timespinnerTracker.html
new file mode 100644
index 00000000..b0d50b58
--- /dev/null
+++ b/WebHostLib/templates/timespinnerTracker.html
@@ -0,0 +1,82 @@
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+
+
+
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index 1e0a91ce..5bda11b8 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -333,6 +333,8 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
return __RenderMinecraftTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
elif games[tracked_player] == "Ocarina of Time":
return __RenderOoTTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
+ elif games[tracked_player] == "Timespinner":
+ return __RenderTimespinnerTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
else:
return __RenderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name)
@@ -680,6 +682,89 @@ def __RenderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
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]]],
+ inventory: Counter, team: int, player: int, playerName: str) -> str:
+
+ icons = {
+ "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
+ "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png",
+ "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png",
+ "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png",
+ "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png",
+ "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png",
+ "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png",
+ "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png",
+ "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png",
+ "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png",
+ "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png",
+ "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png",
+ "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png",
+ "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png",
+ "Security Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png",
+ "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png",
+ "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png",
+ "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png",
+ "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png",
+ "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png",
+ "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png",
+ "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png",
+ "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png",
+ "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png",
+ "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png",
+ "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png",
+ "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
+ }
+
+ timespinner_location_ids = {
+ "Present": [
+ 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
+ 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
+ 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
+ 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039,
+ 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049,
+ 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059,
+ 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069,
+ 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079,
+ 1337080, 1337081, 1337082, 1337083, 1337084, 1337085, 1337156, 1337157, 1337159,
+ 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
+ 1337170],
+ "Past": [
+ 1337086, 1337087, 1337088, 1337089,
+ 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099,
+ 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109,
+ 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119,
+ 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129,
+ 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139,
+ 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149,
+ 1337150, 1337151, 1337152, 1337153, 1337154, 1337155],
+ "Ancient Pyramid": [1337246, 1337247, 1337248, 1337249]
+ }
+
+ display_data = {}
+
+ # 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 timespinner_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 timespinner_location_ids.items()}
+ checks_done['Total'] = len(checked_locations)
+ checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()}
+ checks_in_area['Total'] = sum(checks_in_area.values())
+
+ return render_template("timespinnerTracker.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 __RenderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
inventory: Counter, team: int, player: int, playerName: str) -> str: