(() => {
const data = window.VALLEY_DATA;
const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const seasonNames = { spring: "Spring", summer: "Summer", fall: "Fall", winter: "Winter" };
const state = {
year: Number(localStorage.getItem("valley-year")) || 1,
season: localStorage.getItem("valley-season") || "spring",
day: Number(localStorage.getItem("valley-day")) || 1,
filter: "all",
checks: {},
revision: -1,
saving: new Set()
};
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const escapeHtml = (value) => String(value).replace(/[&<>'"]/g, char => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[char]));
const itemKey = crop => crop.sharedId || crop.id;
function saveDate() {
localStorage.setItem("valley-year", state.year);
localStorage.setItem("valley-season", state.season);
localStorage.setItem("valley-day", state.day);
}
function weekday(day = state.day) { return weekdays[(day - 1) % 7]; }
function setDate({ year = state.year, season = state.season, day = state.day }) {
state.year = Math.max(1, Math.min(2, year));
state.season = data.seasons.includes(season) ? season : "spring";
state.day = Math.max(1, Math.min(28, day));
saveDate();
renderDate();
renderToday();
renderCrops();
if (state.filter === "season") renderBundles();
}
function moveDay(delta) {
let index = data.seasons.indexOf(state.season);
let day = state.day + delta;
let year = state.year;
if (day > 28) { day = 1; index += 1; }
if (day < 1) { day = 28; index -= 1; }
if (index > 3) { index = 0; year += 1; }
if (index < 0) { index = 3; year -= 1; }
if (year < 1 || year > 2) return;
setDate({ year, season: data.seasons[index], day });
}
function renderDate() {
document.body.dataset.season = state.season;
$("#season-title").textContent = seasonNames[state.season];
$("#day-title").textContent = state.day;
$("#weekday-title").textContent = `${weekday()} · Year ${state.year}`;
$$("[data-year]").forEach(button => button.setAttribute("aria-pressed", String(Number(button.dataset.year) === state.year)));
$$("[data-season]").forEach(button => button.setAttribute("aria-selected", String(button.dataset.season === state.season)));
$("#day-grid").innerHTML = Array.from({ length: 28 }, (_, index) => {
const day = index + 1;
return ``;
}).join("");
}
function currentBirthday() {
const entry = data.birthdays[state.season][state.day];
if (!entry) return null;
if (entry[1] && entry[1] !== state.year) return null;
return entry[0];
}
function currentEvent() {
const entry = data.events[state.season][state.day];
if (!entry) return null;
if (entry[2] && entry[2] !== state.year) return null;
return entry;
}
function reminders() {
const name = weekday();
const result = [];
if (name === "Friday" || name === "Sunday") result.push(["Traveling Cart", "Cindersap Forest, 6am–8pm. Check for off-season bundle items.", "cart"]);
if (name === "Sunday") result.push(["The Queen of Sauce", "Watch TV for a new recipe during Years 1–2.", "tv"]);
if (name === "Wednesday") result.push(["Recipe rerun", "The Queen of Sauce repeats a recipe you may have missed.", "tv"]);
if (name === "Monday" && (state.year > 1 || state.season !== "spring" || state.day >= 2)) result.push(["New week", "Special Order boards refresh on Monday once unlocked.", "board"]);
return result;
}
function cropDeadlineCards() {
return data.crops.filter(crop => crop.season === state.season && crop.last === state.day && (!crop.year || crop.year === state.year));
}
function card(kind, label, title, copy) {
return `${escapeHtml(label)}
${escapeHtml(title)}
${escapeHtml(copy)}
`;
}
function renderToday() {
const cards = [];
const birthday = currentBirthday();
const event = currentEvent();
if (birthday) cards.push(card("birthday", "Birthday", birthday, "A birthday gift gives 8× the usual friendship effect."));
if (event) cards.push(card("event", "Event", event[0], event[1]));
reminders().forEach(([title, copy]) => cards.push(card("reminder", "Reminder", title, copy)));
cropDeadlineCards().forEach(crop => cards.push(card("deadline", "Plant by today", crop.name, crop.purpose)));
if (!cards.length) cards.push(card("quiet", "Open day", "Make your own luck", "No fixed birthdays, festivals, or last planting dates today."));
$("#today-cards").innerHTML = cards.join("");
const noun = cards.length === 1 ? "thing" : "things";
$("#today-summary").textContent = `${cards.length} ${noun} on the calendar`;
}
function renderCrops() {
const crops = data.crops.filter(crop => crop.season === state.season && (!crop.year || crop.year === state.year));
const rows = crops.map(crop => {
const key = itemKey(crop);
const checked = Boolean(state.checks[key]);
const overdue = state.day > crop.last && !checked;
const today = state.day === crop.last && !checked;
return ``;
});
$("#crop-list").innerHTML = rows.length ? rows.join("") : `
❄No mandatory field crops
Use Winter for the foraging bundle, mines, fishing, and any unfinished animal or artisan goods.
`;
}
function groupProgress(group) {
const done = group.items.filter(entry => state.checks[entry.id]).length;
return { done, required: group.required, complete: done >= group.required };
}
function bundleTotals() {
return data.bundles.reduce((totals, room) => room.groups.reduce((inner, group) => {
const { done, required } = groupProgress(group);
inner.done += Math.min(done, required);
inner.required += required;
return inner;
}, totals), { done: 0, required: 0 });
}
function renderBundles() {
const html = data.bundles.map(room => {
const groups = room.groups.map(group => {
const progress = groupProgress(group);
let items = group.items;
if (state.filter === "season") items = items.filter(entry => !entry.seasons.length || entry.seasons.includes(state.season));
if (state.filter === "open") items = items.filter(entry => !state.checks[entry.id]);
if (!items.length) return "";
return `
${items.map(entry => {
const checked = Boolean(state.checks[entry.id]);
return ``;
}).join("")}
`;
}).filter(Boolean).join("");
if (!groups) return "";
const roomGroups = room.groups.map(groupProgress);
const roomDone = roomGroups.filter(progress => progress.complete).length;
return `${room.icon}${escapeHtml(room.room)}
${roomDone} of ${room.groups.length} bundles ready
${groups}
`;
}).filter(Boolean).join("");
$("#bundle-list").innerHTML = html || `✓Everything in this view is done
Switch filters to review the whole Community Center.
`;
const totals = bundleTotals();
const percent = totals.required ? Math.round(totals.done / totals.required * 100) : 0;
$("#progress-label").textContent = `${totals.done} of ${totals.required} essentials`;
$("#progress-bar").style.width = `${percent}%`;
}
function setSyncStatus(mode, text) {
const node = $("#sync-status");
node.dataset.mode = mode;
node.lastElementChild.textContent = text;
}
function toast(message) {
const node = $("#toast");
node.textContent = message;
node.classList.add("show");
clearTimeout(toast.timer);
toast.timer = setTimeout(() => node.classList.remove("show"), 2600);
}
async function loadState({ quiet = false } = {}) {
if (!quiet) setSyncStatus("loading", "Connecting…");
try {
const response = await fetch("/api/state", { cache: "no-store" });
if (!response.ok) throw new Error(`State request failed (${response.status})`);
const snapshot = await response.json();
if (snapshot.revision !== state.revision) {
state.checks = snapshot.checks || {};
state.revision = snapshot.revision;
renderCrops();
renderBundles();
}
setSyncStatus("ready", "Shared progress synced");
} catch (error) {
console.error(error);
setSyncStatus("error", "Sync unavailable");
}
}
async function updateCheck(itemId, checked) {
const previous = Boolean(state.checks[itemId]);
state.checks[itemId] = checked;
state.saving.add(itemId);
renderCrops();
renderBundles();
setSyncStatus("loading", "Saving…");
try {
const response = await fetch("/api/checks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ itemId, checked })
});
if (!response.ok) throw new Error(`Save failed (${response.status})`);
const result = await response.json();
state.revision = Math.max(state.revision, result.revision);
setSyncStatus("ready", "Shared progress synced");
} catch (error) {
console.error(error);
state.checks[itemId] = previous;
setSyncStatus("error", "Couldn’t save");
toast("That change wasn’t saved. Your previous shared state is restored.");
} finally {
state.saving.delete(itemId);
renderCrops();
renderBundles();
}
}
function installWebMcp() {
const context = document.modelContext;
if (!context?.registerTool) return;
const knownIds = new Set(data.bundles.flatMap(room => room.groups.flatMap(group => group.items.map(entry => entry.id))));
void Promise.resolve(context.registerTool({
name: "get_valley_progress",
title: "Get Valley progress",
description: "Read the currently selected game date and shared Community Center checklist progress.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
annotations: { readOnlyHint: true, untrustedContentHint: false },
execute: () => ({ year: state.year, season: state.season, day: state.day, progress: bundleTotals(), completedItemIds: Object.keys(state.checks).filter(id => state.checks[id]) })
})).catch(console.error);
void Promise.resolve(context.registerTool({
name: "set_valley_checklist_item",
title: "Update Valley checklist",
description: "Check or uncheck one known Community Center item in the shared household plan.",
inputSchema: { type: "object", properties: { itemId: { type: "string" }, checked: { type: "boolean" } }, required: ["itemId", "checked"], additionalProperties: false },
annotations: { readOnlyHint: false, untrustedContentHint: false },
async execute(input) {
if (!input || !knownIds.has(input.itemId) || typeof input.checked !== "boolean") throw new Error("Unknown checklist item or invalid checked value");
await updateCheck(input.itemId, input.checked);
return { itemId: input.itemId, checked: Boolean(state.checks[input.itemId]), revision: state.revision };
}
})).catch(console.error);
}
document.addEventListener("click", event => {
const yearButton = event.target.closest("[data-year]");
if (yearButton) setDate({ year: Number(yearButton.dataset.year) });
const seasonButton = event.target.closest("[data-season]");
if (seasonButton) setDate({ season: seasonButton.dataset.season });
const dayButton = event.target.closest("[data-day]");
if (dayButton) setDate({ day: Number(dayButton.dataset.day) });
const filterButton = event.target.closest("[data-filter]");
if (filterButton) {
state.filter = filterButton.dataset.filter;
$$("[data-filter]").forEach(button => button.setAttribute("aria-pressed", String(button === filterButton)));
renderBundles();
}
const navLink = event.target.closest(".section-nav a");
if (navLink) {
$$(".section-nav a").forEach(link => link.classList.toggle("active", link === navLink));
}
});
document.addEventListener("change", event => {
const input = event.target.closest("[data-check-id]");
if (input) updateCheck(input.dataset.checkId, input.checked);
});
$("#previous-day").addEventListener("click", () => moveDay(-1));
$("#next-day").addEventListener("click", () => moveDay(1));
renderDate();
renderToday();
renderCrops();
renderBundles();
loadState();
setInterval(() => loadState({ quiet: true }), 4000);
installWebMcp();
})();