diff --git a/README.md b/README.md
index 22e54d9..807782c 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,8 @@ A small, shared tracker for the first two years of Stardew Valley. It combines:
Every actionable card and checklist row uses the same shared checkbox behavior. Date and season selection remain local to each device, so two players can look at different days without moving each other's calendar.
+Community Center filters distinguish all unfinished items, items available in the selected season, season-exclusive items, and fish that require a rain or sun window. Choose-N bundles stop treating unused alternatives as required once their target is met.
+
The app is deliberately dependency-free at runtime: Python's standard library serves the site and stores progress in SQLite. Nginx/Authelia are expected to provide the public access boundary.
## Run locally
diff --git a/flake.nix b/flake.nix
index 6ace621..9b5a58c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,7 +31,9 @@
node --check static/app.js
node --check static/data.js
node --check static/sync.js
+ node --check static/availability.js
node tests/test_sync.js
+ node tests/test_availability.js
touch $out
'';
});
diff --git a/package.nix b/package.nix
index 23f453a..c94477e 100644
--- a/package.nix
+++ b/package.nix
@@ -2,7 +2,7 @@
stdenvNoCC.mkDerivation {
pname = "stardew-tracker";
- version = "1.1.0";
+ version = "1.2.0";
src = ./.;
nativeBuildInputs = [ makeWrapper ];
diff --git a/static/app.js b/static/app.js
index 6a3ac8c..c401e33 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,6 +1,7 @@
(() => {
const data = window.VALLEY_DATA;
const sync = window.ValleySync;
+ const availability = window.ValleyAvailability;
const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const seasonNames = { spring: "Spring", summer: "Summer", fall: "Fall", winter: "Winter" };
const state = {
@@ -34,7 +35,7 @@
renderDate();
renderToday();
renderCrops();
- if (state.filter === "season") renderBundles();
+ renderBundles();
}
function moveDay(delta) {
@@ -149,19 +150,60 @@
}, 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 === "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 (state.filter !== "all") items = items.filter(entry => availability.matchesFilter(entryStatus(entry, group), state.filter));
if (!items.length) return "";
return ` ${roomDone} of ${room.groups.length} bundles ready${escapeHtml(group.name)}
${group.choose ? `Choose ${group.required}` : ""}${escapeHtml(room.room)}
Switch filters to review the whole Community Center.
${empty[1]}
Availability follows normal sources for the selected season; the Traveling Cart can occasionally provide exceptions.
@@ -94,8 +114,9 @@ - - - + + + +