(() => { const data = window.VALLEY_DATA; const sync = window.ValleySync; const availability = window.ValleyAvailability; const calendar = window.ValleyCalendar; 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(); 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; const birthday = birthdayOn(day); const label = `${seasonNames[state.season]} ${day}, ${weekday(day)}${birthday ? `, ${birthday}'s birthday` : ""}`; const marker = birthday ? `` : ""; return ``; }).join(""); } function birthdayOn(day) { return calendar.birthdayOn(data.birthdays, state.season, day, state.year); } function currentBirthday() { return birthdayOn(state.day); } 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 dailyId(kind, title) { const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 36); return `daily-${state.year}-${state.season}-${state.day}-${kind}-${slug}`; } function giftGuide(name) { const profile = data.giftProfiles[name]; if (!profile) return ""; const giftList = entries => entries.map(entry => `
  • ${escapeHtml(entry)}
  • `).join(""); return `
    Loves
    Avoid
    `; } function card(kind, label, title, copy, itemId = null, extra = "") { const checked = itemId ? Boolean(state.checks[itemId]) : false; const content = `${itemId ? `` : ""}
    ${escapeHtml(label)}

    ${escapeHtml(title)}

    ${escapeHtml(copy)}

    ${extra}`; return itemId ? `` : `
    ${content}
    `; } 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.", dailyId("birthday", birthday), giftGuide(birthday))); if (event) cards.push(card("event", "Event", event[0], event[1], dailyId("event", event[0]))); reminders().forEach(([title, copy]) => cards.push(card("reminder", "Reminder", title, copy, dailyId("reminder", title)))); cropDeadlineCards().forEach(crop => cards.push(card("deadline", "Plant by today", crop.name, crop.purpose, itemKey(crop)))); 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 · tick cards when done`; } 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 entryStatus(entry, group) { const progress = groupProgress(group); return availability.classify(entry, { checked: Boolean(state.checks[entry.id]), chooseGroupComplete: Boolean(group.choose && progress.complete), season: state.season }); } function allEntryStatuses() { return data.bundles.flatMap(room => room.groups.flatMap(group => group.items.map(entry => ({ entry, group, status: entryStatus(entry, group) })) )); } function renderPrioritySummary() { const entries = allEntryStatuses(); const count = filter => entries.filter(({ status }) => availability.matchesFilter(status, filter)).length; $("#needed-now-count").textContent = count("now"); $("#exclusive-count").textContent = count("exclusive"); $("#weather-count").textContent = count("weather"); $("#needed-now-detail").textContent = `Available in ${seasonNames[state.season]}`; $("#exclusive-detail").textContent = `Only in ${seasonNames[state.season]}`; } function seasonList(seasons) { return seasons.map(season => seasonNames[season]).join(" + "); } function statusBadges(status) { if (status.checked) return ""; const badges = []; if (!status.needed) badges.push(`Bundle ready`); else if (status.seasonExclusive) badges.push(`Only ${seasonNames[state.season]}`); else if (status.inSeason) badges.push(`In season`); else if (status.seasonal) badges.push(`${seasonList(status.seasons)}`); if (status.weather) badges.push(`${status.weather === "Rain" ? "Rain needed" : "Sunny only"}`); return badges.length ? `${badges.join("")}` : ""; } 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 !== "all") items = items.filter(entry => availability.matchesFilter(entryStatus(entry, group), state.filter)); if (!items.length) return ""; return `

    ${escapeHtml(group.name)}

    ${group.choose ? `Choose ${group.required}` : ""}
    ${Math.min(progress.done, progress.required)}/${progress.required}
    ${items.map(entry => { const checked = Boolean(state.checks[entry.id]); const status = entryStatus(entry, group); const classes = [checked ? "complete" : "", status.needed ? "needed" : "optional", status.needed && status.availableNow ? "available-now" : "", status.seasonExclusive ? "season-exclusive" : "", status.weather ? "weather-specific" : ""].filter(Boolean).join(" "); 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(""); const emptyMessages = { now: ["Nothing needed right now", `No unfinished bundle items are available from normal sources in ${seasonNames[state.season]}.`], exclusive: ["No season-only items left", `You have covered every unfinished item exclusive to ${seasonNames[state.season]}.`], weather: ["No weather windows left", `No unfinished fish in ${seasonNames[state.season]} require rain or sun.`], open: ["Community Center covered", "No required bundle items remain."], all: ["Everything in this view is done", "Switch filters to review the whole Community Center."] }; const empty = emptyMessages[state.filter] || emptyMessages.all; $("#bundle-list").innerHTML = html || `
    ✓

    ${empty[0]}

    ${empty[1]}

    `; renderPrioritySummary(); 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(); const merged = sync.mergeSnapshot(state.checks, state.saving, state.revision, snapshot); if (merged.applied) { state.checks = merged.checks; state.revision = merged.revision; renderToday(); 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); renderToday(); 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(); const confirmed = sync.confirmWrite(state.checks, state.revision, result); state.checks = confirmed.checks; state.revision = confirmed.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."); void loadState({ quiet: true }); } finally { state.saving.delete(itemId); renderToday(); 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("button[data-year]"); if (yearButton) setDate({ year: Number(yearButton.dataset.year) }); const seasonButton = event.target.closest("button[data-season]"); if (seasonButton) setDate({ season: seasonButton.dataset.season }); const dayButton = event.target.closest("button[data-day]"); if (dayButton) setDate({ day: Number(dayButton.dataset.day) }); const filterButton = event.target.closest("button[data-filter]"); if (filterButton) { state.filter = filterButton.dataset.filter; $$("[data-filter]").forEach(button => button.setAttribute("aria-pressed", String(button.dataset.filter === state.filter))); 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(); })();