let ip = null; fetch("https://api.ipify.org?format=json").then((response) => { if (response.ok) { return response.json(); } throw new Error("Network response was not ok."); }).then((res) => { ip = res.ip; }).catch((_error) => { if ("caches" in window) { caches.match("https://api.ipify.org?format=json").then((response) => { if (response) { return response.json(); } }).then((data) => { if (ip === null && data !== undefined) { ip = JSON.parse(data).ip; } }); } }); let goodFortunes = []; let badFortunes = []; let special_events = []; let commit_hash = ""; // using async and await to prevent fetching the data too late... async function fetch_data(requied_commit_hash) { let prefix = ""; if (requied_commit_hash) { prefix = `https://raw.githubusercontent.com/LifeAdventurer/generators/${requied_commit_hash}/fortune_generator/`; } await fetch(`${prefix}./json/fortune.json`) .then((response) => response.json()) .then((data) => { goodFortunes = data.goodFortunes; badFortunes = data.badFortunes; }); await fetch('./json/commit_hash.json') .then((response) => response.json()) .then((data) => { commit_hash = data.commit_hash; }); async function fetch_events(path) { await fetch(path) .then((response) => response.json()) .then((data) => { special_events.push(...data.special_events); }); } await fetch_events(`${prefix}./json/custom_special.json`); await fetch_events(`${prefix}./json/static_special.json`); await fetch_events(`${prefix}./json/cyclical_special.json`); } const textColorClass = [ "good-fortune", "good-fortune", "good-fortune", "good-fortune", "good-fortune", "middle-fortune", "bad-fortune", "bad-fortune", ]; const fortuneStatus = [ "大吉", "中吉", "小吉", "吉", "末吉", "中平", "凶", "大凶", ]; const chineseMonth = [ "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二", ]; const week = ["日", "一", "二", "三", "四", "五", "六"]; const title = `今日運勢`; const allGood = `萬事皆宜`; const allBad = `諸事不宜`; // date const d = new Date(); const date = d.getDate(); const day = d.getDay(); const month = d.getMonth() + 1; const year = d.getFullYear(); function validateNumber(value, min, max, fieldName, event) { value = parseInt(value); if (isNaN(value) || value < min || value > max) { console.warn( `illegal event: ${fieldName} should be between ${min} and ${max}`, event, ); return null; } return value; } function isLeapYear(year) { if (year % 400 === 0) return true; if (year % 100 === 0) return false; if (year % 4 === 0) return true; return false; } const daysPerMonth = [ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, ]; const maxDate = new Date(8640000000000000); function daysDiff(eventIndex) { // define the date right now and the special event date const event = special_events[eventIndex]; const startDate = new Date(year, month - 1, date); let eventYear = -1, eventMonth = -1, eventDate = -1; if (!("triggerDate" in event)) { console.warn("illegal event: missing `triggerDate` field", event); return -1; } else if ( Object.prototype.toString.call(event.triggerDate) !== "[object Object]" ) { console.warn( "illegal event: `triggerDate` field should be a json object", event, ); return -1; } const triggerDate = event.triggerDate; let isCustomEvent = false; eventYear = year; if ("year" in triggerDate) { eventYear = validateNumber( triggerDate.year, 1, maxDate.getFullYear(), "triggerDate.year", event, ); if (eventYear === null) { return -1; } isCustomEvent = true; } if (!("month" in triggerDate)) { console.warn("illegal event: `triggerDate` missing `month` field", event); return -1; } eventMonth = validateNumber( triggerDate.month, 1, 12, "triggerDate.Month", event, ); if (eventMonth === null) { return -1; } if ( !("date" in triggerDate) && (!("week" in triggerDate) || !("weekday" in triggerDate)) ) { console.warn( "illegal event: `triggerDate` require (`week` and `weekday`) or `date` field", event, ); return -1; } if ("date" in triggerDate) { let days = daysPerMonth[eventMonth]; if (isLeapYear(eventYear) && eventMonth == 2) days += 1; eventDate = validateNumber( triggerDate.date, 1, days, "triggerDate.date", event, ); if (eventDate === null) { return -1; } } else { triggerDate.week = validateNumber( triggerDate.week, 1, 5, "triggerDate.week", event, ); triggerDate.weekday = validateNumber( triggerDate.weekday, 1, 7, "triggerDate.weekday", event, ); if (triggerDate.week === null || triggerDate.weekday === null) { return -1; } const firstDayOfMonth = new Date(eventYear, eventMonth - 1, 1); const firstDayWeekday = firstDayOfMonth.getDay(); // Sunday -> 7 const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday; const firstTargetDay = 1 + (triggerDate.weekday - adjustedFirstDayWeekday + 7) % 7; eventDate = firstTargetDay + (triggerDate.week - 1) * 7; } if ( !isCustomEvent && (month > eventMonth || (month == eventMonth && date > eventDate)) ) { eventYear += 1; } const endDate = new Date( eventYear, eventMonth - 1, eventDate, ); // calculate the difference in milliseconds and convert it to days const timeDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)); return timeDiff; } // pre-search jquery - save to a variable to improve performance const J_l_1_event = $("#l-1-event"); const J_l_1_desc = $("#l-1-desc"); const J_l_2_event = $("#l-2-event"); const J_l_2_desc = $("#l-2-desc"); const J_r_1_event = $("#r-1-event"); const J_r_1_desc = $("#r-1-desc"); const J_r_2_event = $("#r-2-event"); const J_r_2_desc = $("#r-2-desc"); const J_ip_to_fortune = $("#ip-to-fortune"); let special = false; let special_events_index = -1; let l1 = -1, l2 = -1, r1 = -1, r2 = -1; let status_index = -1; let seed1 = -1, seed2 = -1; let fortune_generated = false; let preview_result = false; let current_day_special_events = []; // init page async function init_page() { let urlParams = new URLSearchParams(window.location.search); let commit_hash = null; if (urlParams.has('fi') && urlParams.has('si') && urlParams.has('ei'), urlParams.has('ch')) { // fortune_index, status_index, event_index, commit_hash status_index = parseInt(urlParams.get('si')); special_events_index = parseInt(urlParams.get('ei')); [l1, l2, r1, r2] = urlParams.get('fi').split(':').map(num => parseInt(num)); commit_hash = urlParams.get('ch'); if (isNaN(status_index) || isNaN(special_events_index) || isNaN(l1) || isNaN(l2) || isNaN(r1) || isNaN(r2)) { special_events_index = -1; l1 = -1, l2 = -1, r1 = -1, r2 = -1; status_index = -1; commit_hash = null; } else { preview_result = true; if (special_events_index != -1) special = true; } } // fetch data from `fortune.json` await fetch_data(commit_hash); // hide the elements of show fortune page $("#result-page").hide(); // show date before button pressed const showMonth = `${ chineseMonth[month - 1] + "月" }`; const showDate = `${ ("0" + date).slice(-2) }`; const showDay = `${ "星期" + week[day] }`; $("#month").html(showMonth); $("#date").html(showDate); $("#weekday").html(showDay); if (preview_result) Appear(); if (!preview_result) { const showSpecialEventCount = 2; let eventIndexList = Array(showSpecialEventCount).fill(-1); let eventDiffDaysIndexList = Array(showSpecialEventCount).fill( Number.MAX_SAFE_INTEGER, ); // check if there is special event today for (let i = 0; i < special_events.length; i++) { let diffCount = daysDiff(i); if (diffCount > 0) { let j = 0; for (; j < showSpecialEventCount; j++) { if (diffCount < eventDiffDaysIndexList[j]) { break; } } eventDiffDaysIndexList[j] = diffCount; eventIndexList[j] = i; } else if (diffCount === 0) { special = true; current_day_special_events.push(i); } } // if there is upcoming event then show for (let eventIndex = 0; eventIndex < showSpecialEventCount; eventIndex++) { if (eventIndexList[eventIndex] != -1) { const days = daysDiff(eventIndexList[eventIndex]); const upcoming_event = `距離${ special_events[eventIndexList[eventIndex]].event }還剩${days}`; $(`#upcoming-event-${eventIndex + 1}`).html(upcoming_event); } } const last_date_str = localStorage.getItem("last_date"); if (last_date_str !== null && last_date_str !== undefined) { const now_date = new Date(); const last_date = new Date(last_date_str); if ( now_date.getFullYear() === last_date.getFullYear() && now_date.getMonth() === last_date.getMonth() && now_date.getDate() === last_date.getDate() ) { fortune_generated = true; } } if (fortune_generated) { Update(); if (special) { special_events_index = parseInt(localStorage.getItem("last_special_index")); } } else { if (current_day_special_events.length) { special_events_index = ip.split(".").map(num => parseInt(num)).reduce((acc, cur) => acc + cur); special_events_index %= current_day_special_events.length; special_events_index = current_day_special_events[special_events_index]; localStorage.setItem("last_special_index", special_events_index); } } // show special event if today is a special day if (special) { const special_event_today = `今日是${ special_events[special_events_index].event }`; $("#special-day").html(special_event_today); } } } // event bar const good_span = (event) => `宜: ${event}`; const bad_span = (event) => `忌: ${event}`; const desc_span = (desc) => `${desc}`; function Appear() { $("#title").html(title); $("#btn").html("打卡成功"); // disable the btn $("#btn").attr("disabled", "disabled"); //change page $("#init-page").hide(); $("#result-page").show(); // some lengths const goodLen = goodFortunes.length; const badLen = badFortunes.length; const statusLen = fortuneStatus.length; if (!fortune_generated && !preview_result) { // transform ip to four numbers const num = ip.split(".").map((num) => parseInt(num)); // TODO: improve the hash process const hashDate = Math.round( Math.log10( year * ((month << (Math.log10(num[3]) + day - 1)) * (date << Math.log10(num[2] << day))), ), ); seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) + (num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) + (year * day) >> 2; seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) + (num[2] << 1) * (num[3] << 2) + (date << (hashDate - 1)) * (month << 4) + year >> hashDate + (date * day) >> 1; // decide the status let seedMagic = 0; if (seed1 > seed2) { seedMagic = (seed1 ^ seed2) + parseInt(seed1.toString().split("").reverse().join("")); } else if (seed1 < seed2) { let collatzLen = 0; let temp = Math.abs(seed1 - seed2); while (temp !== 1) { temp = temp % 2 === 0 ? temp / 2 : 3 * temp + 1; collatzLen++; } seedMagic = collatzLen + seed2.toString(2).replace(/0/g, "").length; } else { seedMagic = seed1 + seed2; } status_index = (seedMagic % statusLen + statusLen) % statusLen; // update last record localStorage.setItem("last_date", d.toISOString()); localStorage.setItem("last_status_index", status_index.toString()); localStorage.setItem("last_seed1", seed1.toString()); localStorage.setItem("last_seed2", seed2.toString()); } else if (!preview_result) { status_index = parseInt(localStorage.getItem("last_status_index")); seed1 = parseInt(localStorage.getItem("last_seed1")); seed2 = parseInt(localStorage.getItem("last_seed2")); } const status = `§ ${fortuneStatus[status_index]} §`; if (special) { status_index = special_events[special_events_index].status_index; const special_status = `§ ${ fortuneStatus[status_index] } §`; J_ip_to_fortune.html(special_status); } else { J_ip_to_fortune.html(status); } // make sure the events won't collide if (!preview_result) { const set = new Set(); l1 = (seed1 % goodLen + goodLen) % goodLen; set.add(goodFortunes[l1].event); l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen; while (set.has(goodFortunes[l2].event)) { l2 = (l2 + 1) % goodLen; } set.add(goodFortunes[l2].event); r1 = (((seed1 >> 2) + ((month * 42 + year) << 3 + 3) + 19) % badLen + badLen) % badLen; if ( r1 == 0 && (Math.abs(seed1) % 2 === Math.abs(seed2) % 2 || seed1 % 2 === 0 || seed2 % 3 === 1) ) { r1 = (r1 + (Math.abs(seed1 - seed2) % 100) >> 4) % badLen; } while (set.has(badFortunes[r1].event)) { r1 = (r1 + 7) % badLen; } set.add(badFortunes[r1].event); r2 = (((((seed1 << 3 + 7) + (year >> 5) * (date << 2 + 3)) * seed2) >> 4 + seed2 % 42) % badLen + badLen) % badLen; if ( r2 == 0 && (Math.abs(seed1) % 3 % 2 === Math.abs(seed2) % 3 % 2 || seed1 % 3 === seed2 % 2 || (month % 3 === 1 && year % 2 === 1) || month % 4 === 3 || date % 7 === 2) ) { r2 = ((r2 - (Math.abs(seed1 + seed2) % 10) >> 1) % badLen + badLen) % badLen; } while (set.has(badFortunes[r2].event)) { r2 = (r2 + 17) % badLen; } } // organize the stuffs below this line... const l1_desc_list = goodFortunes[l1].description; const l2_desc_list = goodFortunes[l2].description; const r1_desc_list = badFortunes[r1].description; const r2_desc_list = badFortunes[r2].description; const l_1_event = good_span(goodFortunes[l1].event); const l_1_desc = desc_span( l1_desc_list[Math.abs(seed1) % l1_desc_list.length], ); const l_2_event = good_span(goodFortunes[l2].event); const l_2_desc = desc_span( l2_desc_list[Math.abs(seed2) % l2_desc_list.length], ); const r_1_event = bad_span(badFortunes[r1].event); const r_1_desc = desc_span( r1_desc_list[Math.abs(seed1) % r1_desc_list.length], ); const r_2_event = bad_span(badFortunes[r2].event); const r_2_desc = desc_span( r2_desc_list[Math.abs(seed2) % r2_desc_list.length], ); if (special) { // instead clear variable name, use short variable name for here... cuz it's too repetitive const Data = special_events[special_events_index]; if (status_index == 0) { J_r_1_event.html(allGood); } else { J_r_1_event.html(bad_span(Data.badFortunes.r_1_event)); J_r_1_desc.html(desc_span(Data.badFortunes.r_1_desc)); J_r_2_event.html(bad_span(Data.badFortunes.r_2_event)); J_r_2_desc.html(desc_span(Data.badFortunes.r_2_desc)); if (Data.badFortunes.r_1_event.length == 0) { J_r_1_event.html(r_1_event); J_r_1_desc.html(r_1_desc); } if (Data.badFortunes.r_2_event.length == 0) { J_r_2_event.html(r_2_event); J_r_2_desc.html(r_2_desc); } } if (status_index == statusLen - 1) { J_l_1_event.html(allBad); } else { J_l_1_event.html(good_span(Data.goodFortunes.l_1_event)); J_l_1_desc.html(desc_span(Data.goodFortunes.l_1_desc)); J_l_2_event.html(good_span(Data.goodFortunes.l_2_event)); J_l_2_desc.html(desc_span(Data.goodFortunes.l_2_desc)); if (Data.goodFortunes.l_1_event.length == 0) { J_l_1_event.html(l_1_event); J_l_1_desc.html(l_1_desc); } if (Data.goodFortunes.l_2_event.length == 0) { J_l_2_event.html(l_2_event); J_l_2_desc.html(l_2_desc); } } } else { if (status_index == 0) { J_r_1_event.html(allGood); } else { J_r_1_event.html(r_1_event); J_r_1_desc.html(r_1_desc); J_r_2_event.html(r_2_event); J_r_2_desc.html(r_2_desc); } if (status_index == statusLen - 1) { J_l_1_event.html(allBad); } else { J_l_1_event.html(l_1_event); J_l_1_desc.html(l_1_desc); J_l_2_event.html(l_2_event); J_l_2_desc.html(l_2_desc); } } $("#copy-result-button").removeClass("d-none"); $("#copy-preview-result-url-button").removeClass("d-none"); } function copyPreviewResultUrlToClipboard() { let baseUrl = location.href.split("?")[0]; let url = `${baseUrl}?si=${status_index}&ei=${special_events_index}&fi=${[l1,l2,r1,r2].join(":")}&ch=${commit_hash.substr(0, 7)}`; navigator.clipboard.writeText(url); showCopiedNotice(); } function getLuck() { Update(); } init_page();