202 lines
12 KiB
Markdown
202 lines
12 KiB
Markdown
# Routines that reset with the game's version — design
|
||
|
||
Date: 2026-10-22. Status: **shipped** on branch `feat/version-routines` (`34ebc40..5896819`), approved by the owner 2026-10-22.
|
||
|
||
## Problem
|
||
|
||
Wuthering Waves' shop sells gacha pulls with a limit of 7 Radiant Tide, 7 Forging Tide and 7
|
||
Lustrous Tide per version, and the stock refills when the next version arrives (owner, 2026-10-22).
|
||
The in-game timer read **19d 20h** at about 14:30 UTC on 2026-10-22, which puts the restock at
|
||
about 10:30 UTC on 11 November 2026. That matches the owner's reading of 16 October ("resets each
|
||
version, 11 Nov ~10–11 UTC", built-in routines spec § Scope item 3) and the community estimates for
|
||
3.8's launch (Game8, u7buy: 11–12 November).
|
||
|
||
No built-in period can express "until the next version". Weeks and months are fixed, and a
|
||
`cycle` assumes every version has the same length. This change adds version timers as a concept,
|
||
so routines can follow them. The owner asked for exactly that: "We should start tracking version
|
||
timers to allow routines to follow after them."
|
||
|
||
## Decisions
|
||
|
||
The owner's, from the conversation on 2026-10-22:
|
||
|
||
1. **Track version timers per game**, so that a routine can reset when a version ends.
|
||
2. **When the current version's end is unknown**, the routine says *Resets with next version
|
||
(<number>)* and gives an estimate.
|
||
3. **The estimate is shown only.** A routine resets only at an end that is recorded in the table.
|
||
Until someone records the current version's end, the count stays where it is, however far past
|
||
the estimate. This follows AGENTS.md's rule against acting on an invented date.
|
||
4. **A version timer is shown in two places**: through the routines that follow it, and on the
|
||
game's page under Settings → *Your games*. It gets no row in the list, no headline, and no
|
||
timeline marker.
|
||
5. **The first three routines** are WuWa's shop pulls: 7 Radiant Tide, 7 Forging Tide and
|
||
7 Lustrous Tide each version.
|
||
|
||
The builder's, marked as such (the owner may overrule them while reading):
|
||
|
||
6. **Versions are a hand-kept table in code**: `src/shared/versions.ts`, held to the same bar as
|
||
`ROUTINES`. Each entry needs a cited page, and figures the owner read in game say so
|
||
("Confirmed in game, YYYY-MM-DD."). No feed source dates WuWa's versions today: Fandom's
|
||
`Version` table stops at 3.5, and Game8 gives both 11 and 12 November (pass windows spec § Not
|
||
shipped). A scraped source can replace entries later; this change does not build one.
|
||
7. **A version ends at an instant, not at a daily reset.** `end` is a UTC timestamp: the moment
|
||
the new version's stock appears, the same on every server. WuWa's daily reset (04:00 server
|
||
time) would roll 11 Nov's game-day at 20:00 UTC on 10 Nov on Asia, about 14 hours before the
|
||
restock. A reader there would see 0/7 when they can't buy yet, and pulls bought that evening
|
||
would be filed under 3.8. The instant comes from the owner's in-game timer, accurate to about
|
||
an hour.
|
||
8. **The version period exists only for built-ins.** `Routine.period` accepts
|
||
`{ unit: "version" }`, but the stored `PeriodSchema` (and so `TaskSchema`, the
|
||
`gacha-tracker:v1:tasks` records and the export format) stays unchanged. An older cached bundle
|
||
never sees an unknown period. *Make it mine* is hidden on a version routine; a reader's own
|
||
version task is a follow-up (§ Not in this change).
|
||
9. **Version routines count amounts only.** A "days ticked" measure needs the list of game-days in
|
||
the period, and an open-ended version has no last day. A test enforces this.
|
||
10. **A version period shows with the cycles.** For grouping it is a `cycle`: the Dailies cadence,
|
||
the *Cycles* group on the routines page, the `GoalStatus.scope`, and the `n/7 · 19d` chip.
|
||
Only the caption differs (§ Display). A fifth cadence would touch every tally in `Dailies.tsx`
|
||
for three rows.
|
||
11. **The estimate is the version's start plus the game's usual version length.** For WuWa that
|
||
is 42 days: 3.7 ran 30 Sep → 11 Nov, and AGENTS.md puts patch cycles at about six weeks. The
|
||
estimate is computed when shown and never stored.
|
||
12. **Shop lines use the existing `pulls` flag.** Radiant Tide (limited character) and Forging
|
||
Tide (limited weapon) are `premium`. Lustrous Tide is `standard`, so it is offered only with
|
||
`prefs.standardPulls`, like Paimon's Bargains' Acquaint Fates.
|
||
|
||
## Shape
|
||
|
||
### `src/shared/versions.ts` (new)
|
||
|
||
```ts
|
||
export interface GameVersion {
|
||
game: GameId;
|
||
number: string; // "3.7": a label, never parsed as a number (3.8 may be followed by 4.0)
|
||
start: string; // ISO UTC instant the version's stock appeared
|
||
end?: string; // ISO UTC instant the next version's stock appears; absent when unknown
|
||
next?: string; // the next version's number, when a page or the owner names it
|
||
source: string; // https page
|
||
checked: string; // YYYY-MM-DD
|
||
note?: string; // "Confirmed in game, 2026-10-22." and similar
|
||
}
|
||
export const VERSIONS: readonly GameVersion[];
|
||
/** Usual length in days, per game, with where the figure comes from in a comment. */
|
||
export const VERSION_DAYS: Partial<Record<GameId, number>>;
|
||
```
|
||
|
||
Plus one pure function, tested at fixed `now`:
|
||
|
||
```ts
|
||
versionWindow(game, now: number) → {
|
||
number: string | undefined; // the version holding now; undefined if not named
|
||
key: string; // the period key amounts are stored under
|
||
start: number; // ms
|
||
end: number | undefined; // ms, only when recorded
|
||
estimate: number | undefined; // ms, start + VERSION_DAYS, when end is not recorded
|
||
nextNumber: string | undefined;
|
||
} | undefined // the game has no versions at all
|
||
```
|
||
|
||
Rules, with the newest entry's ends resolved in order:
|
||
|
||
- If `now` falls inside a listed version with a recorded `end`, that version is the window.
|
||
- If the newest listed version has no `end` and `now` ≥ its `start`, the window is that version,
|
||
open-ended, with `estimate = start + VERSION_DAYS`.
|
||
- If `now` is past the newest recorded `end`, the window is the version that began at that
|
||
`end` (number = that entry's `next`, possibly undefined). It is open-ended, with an estimate
|
||
from that start. Its start is real data, so amounts stored under its key stay where they are
|
||
when the owner adds its entry.
|
||
- The window never rolls on an estimate (decision 3). Past the estimate, it still shows that
|
||
version, and the caption says the end is overdue (§ Display).
|
||
- No `VERSION_DAYS` → no estimate; the caption omits it.
|
||
|
||
**The key** is `` `v${start date in UTC, YYYY-MM-DD}` ``, or the bare `YYYY-MM-DD` if the amounts
|
||
store's key schema requires a date. The builder checks `AmountLog`'s key rule and records which
|
||
one. It depends only on the start instant, so adding the next entry later never moves it.
|
||
|
||
`VERSIONS` is validated by a test: entries per game do not overlap, `end` > `start`, each recorded
|
||
`end` equals the next entry's `start` where both exist, every source is `https`, and both instants
|
||
parse.
|
||
|
||
### First entry
|
||
|
||
| game | number | start | end | next | source | note |
|
||
|---|---|---|---|---|---|---|
|
||
| wuwa | 3.7 | 2026-09-30 (instant per page or owner) | 2026-11-11T10:30:00Z | 3.8 | game8.co/games/Wuthering-Waves/archives/629904 (Version 3.8 Banners and Release Date) | The page estimates 11–12 Nov; the end is the owner's in-game timer, 19d 20h on 22 Oct. Confirmed in game, 2026-10-22. |
|
||
|
||
The builder checks 3.7's start against a page before committing. If no page states it, the builder
|
||
uses 30 Sep (42 days before the end, matching the six-week cycle) and says so in `note`. The start
|
||
affects only the period key.
|
||
|
||
### `Routine` and the period
|
||
|
||
- `RoutineSchema.period` becomes `PeriodSchema.or(z.object({ unit: z.literal("version") }))`.
|
||
`Routine`'s intersection with `TaskShape` has to allow it; the builder decides how (for
|
||
example, a `RoutinePeriod` type).
|
||
- Every consumer of a routine's period (`taskStatus`, `periodOf` callers in `Dailies.tsx`,
|
||
`TimelinePage.tsx`, `RoutineDrawer.tsx`, `taskCaption`, the cadence in
|
||
`state/reader/routines.ts`) handles `version` by going through `versionWindow` with `now`. The
|
||
type checker will list them. In `taskStatus`, an amount version routine reads
|
||
`amounts[window.key]`, and `daysLeft` / `open` are the whole days to `end`, or `null` when no end
|
||
is recorded.
|
||
- `routineToTask` / *Make it mine* is not offered for a version routine.
|
||
|
||
### Display
|
||
|
||
- **Routine caption** (`taskCaption` and the drawer's period line):
|
||
- recorded end: `resets with 3.8 · 11 Nov`
|
||
- no recorded end: `resets with next version (3.9) · expected ~23 Dec`, without the number when
|
||
unknown: `resets with next version · expected ~23 Dec`
|
||
- past the estimate: `resets with next version (3.9) · expected ~23 Dec, not yet confirmed`
|
||
- **Chip**: the cycle form, `n/7 · 19d` with a recorded end, and `n/7` alone without one.
|
||
- **Your games → game → About**: one row under the reset rows, shown only for a game with versions:
|
||
- `Version 3.7 · ends 11 Nov 2026, 11:30 (in 19d)` in the reader's display timezone, with the
|
||
source linked and its `note` beneath it.
|
||
- No recorded end: `Version 3.8 · next version expected ~23 Dec 2026 (estimate)`.
|
||
|
||
## Entries
|
||
|
||
| id | title | period | measure | pulls |
|
||
|---|---|---|---|---|
|
||
| `routine:wuwa:shop-radiant-tide` | Shop: Radiant Tide | version | amount 7 Radiant Tide | premium |
|
||
| `routine:wuwa:shop-forging-tide` | Shop: Forging Tide | version | amount 7 Forging Tide | premium |
|
||
| `routine:wuwa:shop-lustrous-tide` | Shop: Lustrous Tide | version | amount 7 Lustrous Tide | standard |
|
||
|
||
Each has `source` set to a page that describes the shop's pull stock, preferring one that names the
|
||
shop and the currency (the builder finds it). The title names the shop if that page does. Each
|
||
`note` ends "Confirmed in game, 2026-10-22." The limit of 7 per version is the owner's reading.
|
||
|
||
## Not in this change
|
||
|
||
- A scraped version source for WuWa, or wiring `versionTable.ts` / Nikki's `Version` page into
|
||
`VERSIONS`. The table is shaped so that work can fill it later.
|
||
- Version periods on a reader's own tasks, and *Make it mine* for version routines.
|
||
- Version markers on the timeline, or a version row in the list or *Next up*.
|
||
- Moving existing routines (Pioneer Podcast and others) onto the version period.
|
||
|
||
## Upkeep
|
||
|
||
When a version's end is announced or read in game, add it to `VERSIONS` with the next version's
|
||
entry. Until then, version routines hold their count (decision 3). AGENTS.md gets a bullet saying
|
||
so, and TODO.md gets a standing item for 3.8's end.
|
||
|
||
## Testing
|
||
|
||
- `versions.test.ts`: the table invariants; `versionWindow` at 3.7's start, inside it, one minute
|
||
before 2026-11-11T10:30Z, at that instant (3.8, open-ended, estimate 23 Dec), well past the
|
||
estimate (still 3.8, same key), and for a game with no versions. An entry with no `end` gives an
|
||
estimate from its own start.
|
||
- `task.test.ts`: `taskStatus` for a version amount routine with 3/7 entered under 3.7's key (it
|
||
appears; `daysLeft` 20 on 22 Oct), and at 11 Nov 10:30Z (back to 0/7, `daysLeft` null).
|
||
- `routines.test.ts`: the existing invariants pass for the three entries; Lustrous Tide is
|
||
`standard`; every version routine is an amount measure.
|
||
- Caption and chip strings for a recorded end, no end, and past the estimate.
|
||
- A browser check of the routine rows and the About row, at the real date.
|
||
|
||
## Docs
|
||
|
||
`docs/DATA-MODEL.md` (no stored change; note that the version period exists only for built-ins and
|
||
the key it stores amounts under), `docs/PRD.md` F17 (routines can follow a version), AGENTS.md (one
|
||
bullet: versions are hand-kept and cited, estimates are shown and never acted on), TODO.md (the
|
||
Oscillated Coral item is partly addressed; the follow-ups above; 3.8's end), and a changelog entry
|
||
in a follow-up commit, following the repo's habit.
|