diff --git a/src/client/components/YourOwn.tsx b/src/client/components/YourOwn.tsx
index ad3a16f..a0dc130 100644
--- a/src/client/components/YourOwn.tsx
+++ b/src/client/components/YourOwn.tsx
@@ -7,7 +7,7 @@ import { cadenceLabel, EventForm, GameForm } from "./CustomForms.tsx";
import { taskCaption, type Task } from "../../shared/task.ts";
import type { TaskDraft } from "../state/useTasks.ts";
import { TaskForm } from "./TaskForm.tsx";
-import { asOfLabel, type Routine } from "../../shared/routines.ts";
+import { asOfLabel, ownable, type Routine } from "../../shared/routines.ts";
/**
* The reader's own games and events — a group of the settings panel (PRD F13).
@@ -349,13 +349,15 @@ export function YourOwn({
>
as of {asOfLabel(routine.asOf)}
- onMakeItMine(routine)}
- className="shrink-0 text-xs text-faint transition-colors hover:text-ink"
- >
- Make it mine
-
+ {ownable(routine) && (
+ onMakeItMine(routine)}
+ className="shrink-0 text-xs text-faint transition-colors hover:text-ink"
+ >
+ Make it mine
+
+ )}
{onTrackRoutine !== undefined && (
{
+ const routine = routinesFor(game).find((r) => r.id === id);
+ return routine !== undefined && ownable(routine);
+};
import { mintTaskId, type Task } from "../../../shared/task.ts";
import {
RoutineDrawer,
@@ -488,9 +493,11 @@ export function RoutinesPage({
)}
{row.id.startsWith("routine:") ? (
- void copy(row)}>
- Make it mine
-
+ builtinOwnable(row.game, row.id) && (
+ void copy(row)}>
+ Make it mine
+
+ )
) : (
row.task && (
<>
diff --git a/src/shared/routines.ts b/src/shared/routines.ts
index b18e41b..03a86e3 100644
--- a/src/shared/routines.ts
+++ b/src/shared/routines.ts
@@ -457,6 +457,45 @@ export const ROUTINES: readonly Routine[] = [
checked: "2026-10-16",
note: "The Hazard Zone; the source says roughly 28 days.",
},
+ // The Oscillated Coral shop's pulls: seven of each, restocked with each
+ // version (owner, in game, 2026-10-22; spec 2026-10-22). The page names the
+ // shop and its three Tides, but is from 2024 and says six a month.
+ {
+ id: "routine:wuwa:shop-radiant-tide",
+ game: "wuwa",
+ title: "Oscillated Coral: Radiant Tide",
+ period: { unit: "version" },
+ pulls: "premium",
+ measure: { kind: "amount", target: 7, unit: "Radiant Tide" },
+ source: "https://samurai-gamers.com/wuthering-waves/wuwa-how-to-get-oscillated-coral/",
+ asOf: "2024-05-31",
+ checked: "2026-10-22",
+ note: "The 2024 page says six a month; the shop now sells seven a version. Confirmed in game, 2026-10-22.",
+ },
+ {
+ id: "routine:wuwa:shop-forging-tide",
+ game: "wuwa",
+ title: "Oscillated Coral: Forging Tide",
+ period: { unit: "version" },
+ pulls: "premium",
+ measure: { kind: "amount", target: 7, unit: "Forging Tide" },
+ source: "https://samurai-gamers.com/wuthering-waves/wuwa-how-to-get-oscillated-coral/",
+ asOf: "2024-05-31",
+ checked: "2026-10-22",
+ note: "The 2024 page says six a month; the shop now sells seven a version. Confirmed in game, 2026-10-22.",
+ },
+ {
+ id: "routine:wuwa:shop-lustrous-tide",
+ game: "wuwa",
+ title: "Oscillated Coral: Lustrous Tide",
+ period: { unit: "version" },
+ pulls: "standard",
+ measure: { kind: "amount", target: 7, unit: "Lustrous Tide" },
+ source: "https://samurai-gamers.com/wuthering-waves/wuwa-how-to-get-oscillated-coral/",
+ asOf: "2024-05-31",
+ checked: "2026-10-22",
+ note: "The 2024 page says six a month; the shop now sells seven a version. Confirmed in game, 2026-10-22.",
+ },
// Infinity Nikki. The wiki's Reset page puts the weekly reset on Monday at
// 04:00 server time and lists both of these among what it renews.
@@ -673,6 +712,13 @@ export function routineToAsk(
* A reader's own task copied from a built-in, remembering where it came from.
* The copy is theirs to edit; the original stays hidden while it exists.
*/
+/**
+ * Whether *Make it mine* is offered: not for a routine that resets with the
+ * version, since a reader's task cannot hold that period (spec 2026-10-22).
+ */
+export const ownable = (routine: Pick): boolean =>
+ routine.period.unit !== "version";
+
export function mineFrom(routine: Routine): {
game: string;
title: string;
diff --git a/test/reader-routines.test.ts b/test/reader-routines.test.ts
index 5000c10..089424c 100644
--- a/test/reader-routines.test.ts
+++ b/test/reader-routines.test.ts
@@ -58,3 +58,27 @@ test("an event the reader marked daily remains a directly tickable routine", ()
expect(row?.placement.followed).toBe(true);
expect(row?.cadence).toBe("day");
});
+
+// Spec 2026-10-22: WuWa's shop pulls reset with the version and show with the cycles.
+import { routinePeriod } from "../src/client/components/redesign/TimelinePage.tsx";
+test("a routine that resets with the version is a cycle row, banded to its recorded end", () => {
+ const now = "2026-10-22T14:30:00.000Z";
+ const s = migrateLegacy({}, ["wuwa"], now, () => "account:one");
+ s.prefs.standardPulls = true;
+ s.data["account:one"]!.amounts[s.accounts["account:one"]!.clocks.at(-1)!.id] = {
+ "routine:wuwa:shop-radiant-tide": { "2026-09-30": { value: 4, at: now } },
+ };
+ const rows = routineRows(s, "account:one", Date.parse(now));
+ const shop = rows.filter((r) => r.id.startsWith("routine:wuwa:shop-"));
+ expect(shop.map((r) => r.cadence)).toEqual(["cycle", "cycle", "cycle"]);
+ const radiant = shop.find((r) => r.id === "routine:wuwa:shop-radiant-tide")!;
+ expect(radiant.status).toMatchObject({ count: 4, required: 7, daysLeft: 20, periodStart: "2026-09-30" });
+ expect(routinePeriod(radiant, Date.parse(now))).toEqual({
+ start: Date.parse("2026-09-30T03:00:00Z"),
+ end: Date.parse("2026-11-11T10:30:00Z"),
+ });
+ // Past the recorded end, drawn to the estimate — a picture, not a reset.
+ expect(routinePeriod(radiant, Date.parse("2026-11-20T00:00:00Z")).end).toBe(
+ Date.parse("2026-12-23T10:30:00Z"),
+ );
+});
diff --git a/test/redesign-routine-drawer.test.tsx b/test/redesign-routine-drawer.test.tsx
index a108721..ece6fa2 100644
--- a/test/redesign-routine-drawer.test.tsx
+++ b/test/redesign-routine-drawer.test.tsx
@@ -88,3 +88,10 @@ test("the create shelf asks every mockup question and previews the result", () =
expect(html).toContain(label);
expect(html).toContain("Still needed: a name.");
});
+
+test("a built-in that resets with the version says when, from the version table", () => {
+ // Spec 2026-10-22: the drawer's period line is the version caption.
+ const html = drawer("library", migrateLegacy({}, ["wuwa"], at, () => "account:one"));
+ expect(html).toContain("Oscillated Coral: Radiant Tide");
+ expect(html).toContain("Resets with 3.8 · 11 Nov");
+});
diff --git a/test/redesign-routines.test.tsx b/test/redesign-routines.test.tsx
index 39bd0de..e246188 100644
--- a/test/redesign-routines.test.tsx
+++ b/test/redesign-routines.test.tsx
@@ -118,3 +118,17 @@ test("a finished routine group is marked complete but does not celebrate on arri
expect(html).not.toContain("data-celebrate");
expect(html).not.toContain("fireworks");
});
+
+test("a routine that resets with the version is not offered as Make it mine", () => {
+ // A reader's task cannot hold a version period (spec 2026-10-22).
+ const at = "2026-10-22T14:30:00.000Z";
+ const state = migrateLegacy({}, ["wuwa"], at, () => "account:mine");
+ const pin = { followed: true, checklist: true, timeline: true };
+ state.accounts["account:mine"]!.routines["routine:wuwa:shop-radiant-tide"] = pin;
+ state.accounts["account:mine"]!.routines["routine:wuwa:weekly-challenge"] = pin;
+ const html = render(
+ ({ ok: true })} now={Date.parse(at)} />,
+ );
+ expect(html).toContain("Oscillated Coral: Radiant Tide");
+ expect(html.split("Make it mine").length - 1).toBe(1);
+});
diff --git a/test/routines.test.ts b/test/routines.test.ts
index 223c1d9..91bfb81 100644
--- a/test/routines.test.ts
+++ b/test/routines.test.ts
@@ -102,6 +102,15 @@ describe("ROUTINES", () => {
["Pioneer Podcast weekly EXP", week, { kind: "amount", target: 12000, unit: "EXP" }],
["Weekly Activities", week, { kind: "amount", target: 6000, unit: "points" }],
["Tower of Adversity", { unit: "cycle", days: 28, from: "2026-11-09" }, { kind: "days", required: 1 }],
+ ["Oscillated Coral: Radiant Tide", { unit: "version" }, { kind: "amount", target: 7, unit: "Radiant Tide" }],
+ ["Oscillated Coral: Forging Tide", { unit: "version" }, { kind: "amount", target: 7, unit: "Forging Tide" }],
+ ["Oscillated Coral: Lustrous Tide", { unit: "version" }, { kind: "amount", target: 7, unit: "Lustrous Tide" }],
+ ]);
+ // Limited character and weapon pulls; the standard line only on request.
+ expect(routinesFor("wuwa").filter((r) => r.pulls !== undefined).map((r) => [r.id, r.pulls])).toEqual([
+ ["routine:wuwa:shop-radiant-tide", "premium"],
+ ["routine:wuwa:shop-forging-tide", "premium"],
+ ["routine:wuwa:shop-lustrous-tide", "standard"],
]);
expect(shape("nte")).toEqual([
["Season Pass weekly EXP", week, { kind: "amount", target: 12000, unit: "EXP" }],
diff --git a/test/views.test.tsx b/test/views.test.tsx
index cdc5e72..c0f6f54 100644
--- a/test/views.test.tsx
+++ b/test/views.test.tsx
@@ -1442,6 +1442,21 @@ describe("weekly and monthly tasks in the dailies strip", () => {
expect(longerHeading(new Set(["cycle"]))).toBe("This cycle");
});
+ test("a version task sits with the cycles and has no period to catch up on", () => {
+ // Spec 2026-10-22: there is no "Last version" editor.
+ const shop: StripTask = {
+ id: "routine:wuwa:shop-radiant-tide", game: "wuwa", title: "Oscillated Coral: Radiant Tide",
+ period: { unit: "version" }, measure: { kind: "amount", target: 7, unit: "Radiant Tide" },
+ };
+ const [group] = dailyGroups(["wuwa"], [], NOW, "europe", meta, startOf, false, undefined,
+ [shop], () => cycleStatus);
+ const item = group!.items.find((i) => i.key === shop.id)!;
+ expect(item.cadence).toBe("cycle");
+ expect(item.period).toBeUndefined();
+ expect(tasksShown([shop], { weekly: false, monthly: false, cycle: true })).toEqual([shop]);
+ expect(tasksShown([shop], { weekly: true, monthly: true, cycle: false })).toEqual([]);
+ });
+
test("cycle tasks sit in the lower row, are tallied apart and switched apart", () => {
const [group] = dailyGroups(["zzz"], [], NOW, "europe", meta, startOf, true, undefined,
[points, shiyu], anyStatus);