feat(routines): Wuthering Waves' shop pulls, each version
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b3a84757a1
commit
8cf01b427f
@@ -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)}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMakeItMine(routine)}
|
||||
className="shrink-0 text-xs text-faint transition-colors hover:text-ink"
|
||||
>
|
||||
Make it mine
|
||||
</button>
|
||||
{ownable(routine) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMakeItMine(routine)}
|
||||
className="shrink-0 text-xs text-faint transition-colors hover:text-ink"
|
||||
>
|
||||
Make it mine
|
||||
</button>
|
||||
)}
|
||||
{onTrackRoutine !== undefined && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -6,7 +6,12 @@ import type { CSSProperties } from "react";
|
||||
import { RoutineHistory } from "./RoutineHistory.tsx";
|
||||
import { useState } from "react";
|
||||
|
||||
import { routinesFor, mineFrom } from "../../../shared/routines.ts";
|
||||
import { routinesFor, mineFrom, ownable } from "../../../shared/routines.ts";
|
||||
|
||||
const builtinOwnable = (game: string, id: string) => {
|
||||
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({
|
||||
</a>
|
||||
)}
|
||||
{row.id.startsWith("routine:") ? (
|
||||
<button onClick={() => void copy(row)}>
|
||||
Make it mine
|
||||
</button>
|
||||
builtinOwnable(row.game, row.id) && (
|
||||
<button onClick={() => void copy(row)}>
|
||||
Make it mine
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
row.task && (
|
||||
<>
|
||||
|
||||
@@ -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<Routine, "period">): boolean =>
|
||||
routine.period.unit !== "version";
|
||||
|
||||
export function mineFrom(routine: Routine): {
|
||||
game: string;
|
||||
title: string;
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<RoutinesPage state={state} onAction={async () => ({ ok: true })} now={Date.parse(at)} />,
|
||||
);
|
||||
expect(html).toContain("Oscillated Coral: Radiant Tide");
|
||||
expect(html.split("Make it mine").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user