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)} - + {ownable(routine) && ( + + )} {onTrackRoutine !== undefined && ( + builtinOwnable(row.game, row.id) && ( + + ) ) : ( 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);