1/* The play page around the game: the van stereo (real recordings from the
2 Internet Archive), the tour's tag, and posting a finished run to the tour's
3 public leaderboard with a challenge link to share. Loads before game.js. */
4(function () {
5 "use strict";
6 var B = window.TT_BOOT || {};
7 // Opened inside the archive browser's middle column (desktop): no back link, and
8 // links that leave the game go through the browser page around it.
9 var EMBED = new URLSearchParams(location.search).get("embed") === "1" && window.parent !== window;
10 function tellParent(msg) { if (EMBED) { try { window.parent.postMessage(msg, location.origin); } catch (e) {} } }
11 var $ = function (id) { return document.getElementById(id); };
12 function esc(s) {
13 return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
14 return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
15 });
16 }
17 function ready(fn) { if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn); else fn(); }
18
19 // ------------------------------------------------------------ van stereo
20 // Inside the browser page (desktop) the stereo drives the page's own player, so a
21 // song that was already playing simply keeps going. On its own page it has a
22 // player of its own that picks up the song the browser page was playing.
23 var player = null;
24 function parentPlayer() { try { return EMBED && window.parent.TTPlayer; } catch (e) { return null; } }
25 var TTMusic = {
26 play: function (identifier) { return player ? player.play(identifier) : Promise.resolve(false); },
27 };
28 window.TTMusic = TTMusic;
29 function renderStereo(p) {
30 var np = $("tt-np"); if (!np) return;
31 $("tt-pause").innerHTML = p.playing ? "⏸" : "▶";
32 if (p.blocked) {
33 np.innerHTML = '<button type="button" id="tt-resume">▶ Keep listening</button> ' + esc(p.track ? p.track.title : "");
34 $("tt-resume").onclick = function () { player.toggle(); };
35 } else if (p.loading) np.textContent = "Threading the tape...";
36 else if (p.error) np.innerHTML = esc(p.error) + (p.show ? ' <a href="' + esc(p.show.details) + '" target="_blank" rel="noopener">Open it on the archive ↗</a>' : "");
37 else if (p.track) np.innerHTML = "♫ " + esc(p.track.title) + " · " + esc(p.show.date) + " " + esc(p.show.venue || "") +
38 ' · <a href="' + esc(p.show.details) + '" target="_blank" rel="noopener">tape ↗</a>';
39 }
40
41 ready(function () {
42 player = parentPlayer() || window.TTPlayerCreate($("tt-audio"));
43 var off = player.onChange(renderStereo);
44 window.addEventListener("pagehide", off);
45 if (!parentPlayer()) player.resume(); // came from the browser page: same song, same second
46 $("tt-tonight").onclick = function () {
47 var stop = window.TTGame && window.TTGame.currentStop();
48 if (!stop) stop = B.tour && B.tour.stops[0];
49 if (stop && stop.identifier) TTMusic.play(stop.identifier);
50 else $("tt-np").textContent = "Start a trip first, then the van stereo plays each night's show.";
51 };
52 $("tt-pause").onclick = function () { player.toggle(); };
53 $("tt-next").onclick = function () { player.next(); };
54 document.addEventListener("click", function (e) {
55 var b = e.target.closest && e.target.closest(".ttplay");
56 if (b) { e.preventDefault(); e.stopPropagation(); TTMusic.play(b.getAttribute("data-ident")); }
57 }, true);
58 if (B.donate) document.querySelectorAll(".donate").forEach(function (a) { a.href = B.donate; });
59 if (EMBED) {
60 document.body.classList.add("embedded");
61 $("tt-back").textContent = "\u2715 close game";
62 $("tt-back").onclick = function (e) { e.preventDefault(); tellParent({ type: "tt:close" }); };
63 }
64 if (B.fullTag) {
65 $("tt-tag").textContent = "#" + B.fullTag + (B.rules && B.dna !== "CLASSIC" ? " · " + B.rules : "");
66 $("tt-back").href = "/?tag=" + encodeURIComponent(B.tag) + (B.dna !== "CLASSIC" ? "&dna=" + encodeURIComponent(B.dna) : "");
67 document.title = "#" + B.fullTag + " · Terrapin Trail";
68 }
69 });
70
71 // ------------------------------------------------------------ color scheme
72 // The archive browser's color picker writes the game's own setting; follow it
73 // live when it changes in another tab or in the page around this one.
74 window.addEventListener("storage", function (e) {
75 if (e.key !== "terrapin_opts" || !window.TTGame) return;
76 try { window.TTGame.setTheme(JSON.parse(e.newValue || "{}").theme); } catch (err) {}
77 });
78
79 // ------------------------------------------------------------ the tape playlist
80 // Every real show taped in the game lands in this browser's tape collection,
81 // which the archive browser lists and plays (newest first, capped).
82 window.addEventListener("tt:taped", function (ev) {
83 var t = ev.detail || {};
84 if (!t.identifier) return;
85 try {
86 var tapes = JSON.parse(localStorage.getItem("tt_tapes") || "[]").filter(function (x) { return x.identifier !== t.identifier; });
87 t.tag = B.fullTag || null; t.at = Date.now();
88 tapes.unshift(t);
89 localStorage.setItem("tt_tapes", JSON.stringify(tapes.slice(0, 300)));
90 } catch (e) {}
91 });
92
93 // ------------------------------------------------------------ end of a run
94 function from() { try { return sessionStorage.getItem("tt_from") || "direct"; } catch (e) { return "direct"; } }
95 function savedName() { try { return localStorage.getItem("tt_name") || ""; } catch (e) { return ""; } }
96 function challengeUrl(name, seed) {
97 var q = new URLSearchParams();
98 if (B.dna && B.dna !== "CLASSIC") q.set("dna", B.dna);
99 q.set("luck", seed);
100 q.set("utm_source", "challenge"); q.set("utm_medium", "link"); q.set("utm_campaign", B.fullTag);
101 if (name) q.set("utm_content", name);
102 return location.origin + "/t/" + B.tag + "?" + q.toString();
103 }
104 function copy(text) {
105 if (navigator.clipboard) return navigator.clipboard.writeText(text);
106 return Promise.resolve();
107 }
108
109 window.addEventListener("tt:runend", function (ev) {
110 var rec = ev.detail || {};
111 var box = $("tt-end");
112 $("tt-end-h").textContent = rec.outcome === "won" ? "TOUR COMPLETE" : "END OF THE ROAD";
113 $("tt-end-score").textContent = Number(rec.score || 0).toLocaleString() + " points";
114 var body = $("tt-end-body");
115 if (!B.run) {
116 body.innerHTML = '<p>This tour came from a pasted link, so the score stays on this device. Pick a tour in the archive browser to compete on a shared board.</p>' +
117 '<div class="btns"><a class="b" target="_top" href="/guide/">Archive browser</a><button class="ghost" id="tt-close">See the game\'s end screen</button></div>';
118 } else {
119 body.innerHTML = '<p>Put it on the <b class="groovy">#' + esc(B.fullTag) + "</b> leaderboard" +
120 (B.dna !== "CLASSIC" ? " (" + esc(B.rules) + " rules)" : "") + ".</p>" +
121 '<label for="tt-name">Your name</label><input id="tt-name" maxlength="24" value="' + esc(savedName()) + '">' +
122 '<div class="msg" id="tt-msg"></div>' +
123 '<p class="small"><a class="how" href="/guide/guide/primer/16-fair-play" target="_blank" rel="noopener">How scores are checked</a></p>' +
124 '<div class="btns"><button id="tt-post">Post my score</button><button class="ghost" id="tt-close">Skip</button></div>';
125 $("tt-post").onclick = function () {
126 var name = $("tt-name").value.trim();
127 try { localStorage.setItem("tt_name", name); } catch (e) {}
128 $("tt-msg").textContent = "Posting...";
129 fetch("/api/score", {
130 method: "POST", headers: { "Content-Type": "application/json" },
131 body: JSON.stringify({ run: B.run, name: name, from: from(), record: rec }),
132 }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }).then(function (res) {
133 if (!res.ok) { $("tt-msg").textContent = res.d.error || "Couldn't post that score."; return; }
134 var d = res.d, url = challengeUrl(name, d.entry.seed);
135 tellParent({ type: "tt:board", board: d.board });
136 var text = "I scored " + d.entry.score.toLocaleString() + " on #" + B.fullTag + " (" + B.band + " " + B.year + "). Same luck, your move:";
137 body.innerHTML = '<p>You are <b style="color:#ffb000">#' + d.rank + "</b> of " + d.board.players + " on #" + esc(B.fullTag) + ".</p>" +
138 "<p>Challenge a friend: they get this tour, these rules and the same luck you had.</p>" +
139 '<div class="link" id="tt-link">' + esc(url) + "</div>" +
140 '<div class="btns"><button id="tt-copy">Copy challenge</button>' +
141 '<a class="b ghost" target="_blank" rel="noopener" href="https://twitter.com/intent/tweet?text=' + encodeURIComponent(text) + "&url=" +
142 encodeURIComponent(url.replace("utm_source=challenge", "utm_source=x")) + '">Post on X</a>' +
143 '<a class="b ghost" target="_top" href="/guide/?tag=' + encodeURIComponent(B.tag) + (B.dna !== "CLASSIC" ? "&dna=" + encodeURIComponent(B.dna) : "") + '">Leaderboard</a>' +
144 '<button class="ghost" id="tt-close">Back to the game</button></div>';
145 $("tt-copy").onclick = function () { copy(text + " " + url).then(function () { $("tt-copy").textContent = "Copied"; }); };
146 $("tt-close").onclick = function () { box.classList.remove("on"); };
147 }).catch(function () { $("tt-msg").textContent = "Couldn't reach the server. Try again."; });
148 };
149 }
150 $("tt-close").onclick = function () { box.classList.remove("on"); };
151 // Let the game's own end screen render first, then offer the board on top.
152 setTimeout(function () { box.classList.add("on"); }, 1200);
153 });
154})();
155