const moondrop = {}; // ==================== Configurable stuff ==================== // This maps zoneIDs to the urls of their game, their names and their accent colors moondrop.zoneInfo = { "arcade": {url: "arcade.html", name: "Lunarcade", accent: "rgb(237,218,166)", author: "Joey Jones"}, "fortuneteller": {url: "fortuneteller/fortuneteller2.html", name: "Fortune Teller", accent: "rgb(239,144,141)", author: "Mark Marino"}, "garden": {url: "garden.html", name: "Endymion Gardens", accent: "rgb(210,184,241)", author: "Nils Fagerburg"}, "hotel": {url: "hotel.html", name: "Sanctuary Hotel", accent: "rgb(145,178,226)", author: "Sarah Willson"}, "mall": {url: "mall.html", name: "Gibbous Grove Shopping Center", accent: "rgb(255,154,253)", author: "Zach Hodgens"}, "monorail": {url: "monorail.html", name: "Monorail System", accent: "rgb(208,208,208)", author: "Ryan Veeder"}, "shore": {url: "shore.html", name: "Shore", accent: "rgb(200,237,238)", author: "Carl Muckenhoupt"}, "tunnels": {url: "tunnels.html", name: "Tunnels", accent: "rgb(225,161,130)", author: "Caleb Wilson"}, "waterpark": {url: "waterpark.html", name: "Moonlight Meadow Recreation Complex", accent: "rgb(190,228,192)", author: "Jason Love"}, }; // This defines where a new game starts moondrop.startDestination = "shore.start_beach"; /** Setup initial slot values here This are set the very first time a player plays the game or after a restart. They won't be set when simply reloading. */ moondrop.initialSlotValues = { "monorailPosition": 23, "backpackLocation": "inventory", "silverPinLocation": "backpackPinned", }; /** Map slot names to functions here */ moondrop.calculatedSlots = { "lunarPhase": phase, }; // This is a prefix that the localStorage keys all use (so we can // easily delete them to restart the game without clobbering anything // else that might be stored) const PREFIX = "md_"; // ============================ Code ============================= /** Cross zone travel destination format is zone.room don't use this for travel within a zone as it will reload the page returnRoom is the roomID of the room to which we return when selecting the zone the player is leaving from the map for one-way transitions you can leave returnRoom undefined */ moondrop.goto = (destination, returnRoom) => { const [zone, room] = [...destination.split(".")]; if (returnRoom) setZoneReturnRoom(readSlot("zone"), returnRoom); setZoneReturnRoom(zone, room); saveSlot("zone", zone); saveSlot("room", room); const zoneInfo = moondrop.zoneInfo[zone]; if (zoneInfo.author) document.getElementById("zoneTitle").innerHTML = zoneInfo.name +"
by " + zoneInfo.author; else document.getElementById("zoneTitle").innerHTML = ""; document.getElementById("badge").src = `r/${zone}badge.png`; setAccentColor(zoneInfo.accent ?? "var(--black)"); loadFrame(zoneInfo.url); }; /** Returns the current room ID (without the zone prefix) */ moondrop.currentRoom = () => readSlot("room"); /** Write an arbitrary value to storage slot is a string (text) that names the value zone is also a string, it's the zoneid of the current game the rationale here is to give each author a private space to store data without having to worry about name collisions */ moondrop.write = (zone, slot, value) => { if (zone == "COMMON" && moondrop.calculatedSlots[slot]) return moondrop.calculatedSlots[slot](value); saveSlot(`${zone}_${slot}`, value); } /** Read a value from storage */ moondrop.read = (zone, slot) => { if (zone == "COMMON" && moondrop.calculatedSlots[slot]) return moondrop.calculatedSlots[slot](); return readSlot(`${zone}_${slot}`); } /** Read a number from storage Like `moondrop.read` but coerces missing values to 0 */ moondrop.readNumber = (zone, slot) => { const val = Number(moondrop.read(zone, slot)); if (val == NaN) throw new Error(`Attempt to read a non-number from ${zone}.${slot} as a number.`); return val; } /** Restart the game completely */ moondrop.restart = () => { Object.keys(localStorage).filter(i => i.startsWith(PREFIX)).forEach(k => localStorage.removeItem(k)); moondrop.startTranscript(); document.getElementById("badge").src = `r/mapbadge.png`; setAccentColor("reset"); loadPage(); } /** Returns a list of zoneIDs of the visited zones probably only useful for the map page */ moondrop.discoveredZones = () => Object.keys(zoneRRMap()); /** Return to a zone, probably only useful for the map page zone argument is a zoneID */ moondrop.gotoZone = zone => { moondrop.goto(`${zone}.${zoneRRMap()[zone]}`); } moondrop.newGame = () => { moondrop.goto(moondrop.startDestination); } moondrop.startTranscript = () => { const hash = document.getElementById("debug")?.innerText.split("\n")[0] ?? "???"; moondrop.transcript = "Moondrop Isle\n-------- ----\n(version: " + hash + ")\n\n"; } moondrop.downloadTranscript = () => { const d = new Date(); const fn = "moondrop isle transcript " + d.getDate() + "-" + (d.getMonth()+1) + "-" + d.getFullYear() + " " + d.getHours() + "" + d.getMinutes().toString().padStart(2,"0") + ".txt"; const f = new File([...moondrop.transcript], fn, {type: "octet/stream"}); let url = URL.createObjectURL(f); let a = document.createElement("a"); a.href = url; a.download = fn; a.dispatchEvent(new MouseEvent(`click`, {bubbles: true, cancelable: true, view: window})); }; // ================ Private code that shouldn't be directly called by the games ================ const zoneRRMap = () => readSlot("zoneReturnRooms") || {}; function setZoneReturnRoom(zone, room){ const map = zoneRRMap(); map[zone] = room; saveSlot("zoneReturnRooms", map); } const saveSlot = (slot, value) => { localStorage.setItem(PREFIX + slot, JSON.stringify(value)); } const readSlot = (slot) => JSON.parse(localStorage.getItem(PREFIX + slot)); const loadPage = () => { (function syncSubPageAccent(){ const color = window.getComputedStyle(document.body).getPropertyValue("background-color"); document.getElementById("gameFrame").contentDocument?.body?.style.setProperty("--accent", color); requestAnimationFrame(syncSubPageAccent); })(); moondrop.startTranscript(); if (moondrop.discoveredZones().length == 0){ for (const [prop, value] of Object.entries(moondrop.initialSlotValues)) { let [zone, slot] = prop.split("."); if (!slot){ slot = zone; zone = "COMMON"; } moondrop.write(zone, slot, value); } } const forcedZone = parent.moondrop.read("COMMON","mustReturnTo"); if (!forcedZone) loadFrame("map/index.html"); else moondrop.goto(forcedZone); } /** Load the (possibly relative) url into the frame without messing up the back button */ function loadFrame(url){ document.getElementById("gameFrame").contentWindow.location.replace(new URL(url, document.baseURI)); } function setAccentColor(color){ document.body.style.setProperty("--accent", color); } document.getElementById("gameFrame").addEventListener("load", () => { const vp = document.getElementById("gameFrame").contentWindow.vorple; addDragListeners(document.getElementById("gameFrame").contentDocument.body); if (vp){ const zoneInfo = moondrop.zoneInfo[readSlot("zone")]; moondrop.transcript += `\n === ${zoneInfo.name}${zoneInfo.author ? ` by ${zoneInfo.author}` : ""} ===\n`; //strip funky unicode vp.prompt.addInputFilter((input, meta) => { if (meta.type != "line") return; return input.replace(/[^\x00-\xFF]/g, ""); }); vp.addEventListener("expectCommand", logInform); } }); function logInform(){ let t = document.getElementById("gameFrame")?.contentDocument.querySelector(".turn.previous")?.innerText ?? ""; t = "\n" + t.trim() + "\n"; moondrop.transcript += t; } window.moondrop = moondrop; loadPage(); // extra functions: function phase () { if (arguments[0]) throw new Error("moon phase slot should not be written to!"); // adapted from https://gist.github.com/endel/dfe6bb2fbe679781948c const today = new Date(); let year = today.getFullYear(); let month = today.getMonth()+1; let day = today.getDate(); const phases = ['new-moon', 'waxing-crescent-moon', 'quarter-moon', 'waxing-gibbous-moon', 'full-moon', 'waning-gibbous-moon', 'last-quarter-moon', 'waning-crescent-moon']; let c, e, jd, b; if (month < 3) { year--; month += 12; } ++month; c = 365.25 * year; e = 30.6 * month; jd = c + e + day - 694039.09; jd /= 29.5305882; b = parseInt(jd); jd -= b; b = Math.round(jd * 8); return phases[b%8]; }