feat(routines): let a reader add standard-banner pulls beside the premium ones

Most shops sell both banners' pulls. A shop's premium line is offered like any
built-in; its standard line (Acquaint Fates, Star Rail Pass, Master Tapes,
Resonite Crystals) only when the reader switches on Include standard-banner
pulls under the built-ins switch. A new pref, standardPulls, absent meaning
off, so no install gains anything until it asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-16 03:00:52 +02:00
1 parent 9cf7649cc2
commit af2ad942cd
6 files changed
+167 -7

No files matched your search

+4 -2
View File
@@ -53,6 +53,7 @@ import { goalStatus, readGoal, type GoalStatus } from "../shared/goal.ts";
import { taskStatus, type TaskShape } from "../shared/task.ts";
import {
activeRoutines,
offered,
copyRoutineState,
mineFrom,
routinesFor,
@@ -234,7 +235,8 @@ export function App() {
// Every built-in the reader could track, for the lists that offer them; the
// strip carries only those not stopped one by one.
const routinesHidden = new Set(prefs.routinesHidden ?? []);
const routines = activeRoutines(routineRule(prefs), copiedFrom);
const standardPulls = prefs.standardPulls ?? false;
const routines = activeRoutines(routineRule(prefs), copiedFrom, new Set(), standardPulls);
const stripTasks = [...tasks.tasks, ...routines.filter((r) => !routinesHidden.has(r.id))];
/**
@@ -527,7 +529,7 @@ export function App() {
// A copy the reader made stands in for its built-in, so the original is
// not offered; the copy is theirs, in Your own.
...routinesFor(game)
.filter((r) => !copiedFrom.has(r.id))
.filter((r) => !copiedFrom.has(r.id) && offered(r, standardPulls))
.map((r): StripStanding => ({
id: r.id,
game,
+20 -3
View File
@@ -9,7 +9,7 @@ import { choreRule, routineRule, type Prefs } from "../state/usePrefs.ts";
import { YourOwn } from "./YourOwn.tsx";
import { isCustomGameId } from "../../shared/custom.ts";
import { exceptionLine, setFor, shownFor, type PerGame } from "../../shared/perGame.ts";
import { asOfLabel, routinesFor } from "../../shared/routines.ts";
import { asOfLabel, offered, routinesFor } from "../../shared/routines.ts";
const REGIONS: Array<{ id: Region; label: string }> = (
["america", "europe", "asia"] as const
@@ -103,13 +103,16 @@ export function Controls({
// neither is offered — a chip for it would be a control that does nothing.
const choreGames = games.filter((g) => !prefs.hiddenGames.includes(g) && !isCustomGameId(g));
const routineGames = games.filter((g) => routinesFor(g).length > 0);
const standardPulls = prefs.standardPulls ?? false;
// The standard-banner lines the switch below would add, named in its hint.
const standardLines = routineGames.flatMap((g) => routinesFor(g)).filter((r) => r.pulls === "standard");
// Which periods have a task at all — the reader's own, or a built-in they
// turned on — so a switch is offered only with something behind it.
const periods = [
...(own.tasks ?? []).map((t) => t.period.unit),
...routineGames
.filter((g) => shownFor(routines, g))
.flatMap((g) => routinesFor(g).map((r) => r.period.unit)),
.flatMap((g) => routinesFor(g).filter((r) => offered(r, standardPulls)).map((r) => r.period.unit)),
];
const taskPeriods = { week: periods.includes("week"), month: periods.includes("month") };
@@ -287,7 +290,7 @@ export function Controls({
games={routineGames}
chipsLabel="Built-in weekly and monthly tasks per game"
titleFor={(game) => {
const list = routinesFor(game);
const list = routinesFor(game).filter((r) => offered(r, standardPulls));
// Several pages per game now: quote the oldest, so the
// tooltip never makes a year-old figure look current.
const oldest = list.map((r) => r.asOf).sort()[0]!;
@@ -308,6 +311,20 @@ export function Controls({
/>
)}
{/* Most shops sell both banners' pulls. The premium line comes with
the built-ins; the standard line beside it only on request, one
answer for every game (owner, 2026-10-16). */}
{standardLines.length > 0 && (
<div className="ml-6">
<Check
checked={standardPulls}
onChange={(value) => onUpdate({ standardPulls: value })}
label="Include standard-banner pulls"
hint={`Shops that sell premium pulls mostly sell standard ones too. Off, only the premium line is tracked; on, these join it: ${standardLines.map((r) => r.title).join(", ")}.`}
/>
</div>
)}
{/* Where weekly and monthly tasks sit: their own row under the
dailies. Only offered once there is a task of that period — a
switch with nothing behind it is a control that does nothing. */}
+9
View File
@@ -230,6 +230,12 @@ export interface Prefs {
* numbers stay stored under the id.
*/
routinesHidden?: string[] | undefined;
/**
* Whether a shop's standard-banner pulls are tracked beside its premium ones
* (`Routine.pulls`, owner, 2026-10-16). Absent means no — premium only — so
* an install from before the standard lines existed gains nothing new.
*/
standardPulls?: boolean | undefined;
/**
* The newest changelog entry the reader dismissed (PRD F18). Absent means
* unrecorded, not "seen nothing": first-run setup sets it, so an absent value
@@ -441,6 +447,9 @@ export function restorePrefsValue(incoming: unknown): Prefs {
if (result.showRoutines !== undefined && typeof result.showRoutines !== "boolean") {
result.showRoutines = undefined;
}
if (result.standardPulls !== undefined && typeof result.standardPulls !== "boolean") {
result.standardPulls = undefined;
}
// focusGame
if (result.focusGame !== null && typeof result.focusGame !== "string") {