Compare commits

...
3 Commits
Author SHA1 Message Date
Lucas Winther 8af536a410 chore(changelog): note game order fix and NTE shop routines
CI / Typecheck, test, feed (push) Successful in 21s
CI / Build site (push) Successful in 9s
CI / Container image (push) Successful in 39s
2026-11-02 03:36:18 +01:00
Lucas Winther 0c965e32e9 feat(nte): add monthly Lost Exchange pull routines
Track the shop's five limited dice, five standard dice and twenty weapon keys separately. Keep standard dice behind the existing opt-in and cite the monthly stock and restock evidence.
2026-11-02 03:35:32 +01:00
Lucas Winther dd5d374422 fix: apply game order to dailies and routines
Account creation order leaked into daily rows and routine groups even when the reader had arranged their games. Apply the shared display rule while preserving each game's server and task order.
2026-11-02 03:35:32 +01:00
12 changed files with 135 additions and 9 deletions
+5 -2
View File
@@ -609,7 +609,8 @@ legend on the same baseline as "Next: …".
### Ticking things off
- **Today's dailies.** The whole row is the target. A tick washes the row in its game's hue, pops the
- **Today's dailies.** Rows and progress pips follow Game order, falling back to A–Z by game name;
each game's servers and tasks keep their relative order. The whole row is the target. A tick washes the row in its game's hue, pops the
box and draws the check. A pip bar and "N of M done" sit under the heading. A check-in goal (for
example "5/5 wk") counts as done once met, with a dotted check, and shows its goal count instead of
a streak.
@@ -655,7 +656,9 @@ legend on the same baseline as "Next: …".
### Other pages
- **Routines.** Bands by reset (Dailies, Weeklies, Monthlies, Cycles) or by game. The table has tick
- **Routines.** Bands by reset (Dailies, Weeklies, Monthlies, Cycles) or by game. Game groups,
rows within each reset band, history and Checklist pins follow Game order with the same A–Z fallback.
The table has tick
and click-to-edit amount cells. The library and create/edit drawers are shelves.
- **Timeline.** Two looks (new and classic) chosen in Appearance. Dailies are not on the timeline.
- **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance
@@ -256,7 +256,7 @@ Looked for on 2026-10-14 and not shipped. Each is a candidate the owner may know
| Wuwa | Simulation Training, Forgery | Cost Waveplates; no weekly cap |
| Wuwa | Tower of Adversity Hazard Zone (28 days), Whimpering Wastes | Multi-week → sub-project 2; the owner's in-game dates for both are in § Scope. **Tower of Adversity shipped 2026-10-16** as a 28-day cycle; Whimpering Wastes has no fixed length and stays unshipped ([cycle routines spec](2026-10-16-cycle-routines-design.md) § Not shipped) |
| NTE | Tycoon Incentive Fund (Pink Paws Heist, Volley Star weekly match, Display Case idle earnings) | A 14-day cycle by the game's own guide, not a week → sub-project 2 (§ Scope). The guide this branch cites calls Pink Paws' cap weekly; the game disagrees. The cap also differs per player (Puka Membership Program). **Shipped 2026-10-16** at the base cap with a note ([cycle routines spec](2026-10-16-cycle-routines-design.md), decision 5) |
| NTE | Lost Exchange | Single launch-era source; nothing to count |
| NTE | Lost Exchange | Shipped 2026-11-02: monthly stock of 5 Solid Dice (premium), 5 Fabricated Dice (standard, offered when standard pulls are enabled), and 20 Tri-Keys (weapon pulls). Owner confirmed the counts in game. Icy Veins' item pages state each monthly limit: [Solid Dice](https://www.icy-veins.com/neverness-to-everness/items/dicelimite) and [Tri-Key](https://www.icy-veins.com/neverness-to-everness/items/weapongacha), dated 2026-10-21; [Fabricated Dice](https://www.icy-veins.com/neverness-to-everness/items/dicenormal), read as updated today on 2026-11-02. [Mobalytics](https://mobalytics.gg/neverness-to-everness/guides/daily-weekly-checklist), dated 2026-05-08, states the shops reset on the first day of the month. All read 2026-11-02. |
| NTE | Beyond the Rails | 14-day cycle → sub-project 2; the owner's in-game date is in § Scope. **Shipped 2026-10-16**, on Asia's clock ([cycle routines spec](2026-10-16-cycle-routines-design.md), decision 3) |
**One finding outside this change:** Game8's NTE boss page (2026-10-10) says the weekly timer
+3 -3
View File
@@ -52,7 +52,7 @@ import {
import { metaFor } from "../shared/games.ts";
import { readGoal } from "../shared/goal.ts";
import { isDaily, resolveDaily } from "../shared/daily.ts";
import { orderGames } from "./state/gameOrder.ts";
import { orderGameItems, orderGames } from "./state/gameOrder.ts";
import { advanceFocus } from "./state/lens.ts";
import { restorePrefsValue } from "./state/usePrefs.ts";
import { readableAmounts } from "./state/useAmounts.ts";
@@ -532,12 +532,12 @@ function ReaderContent({
setSettingsPage(next);
setPage("settings");
};
const accounts = Object.values(state.accounts).filter(
const accounts = orderGameItems(Object.values(state.accounts).filter(
(a) =>
!a.retiredAt &&
!prefs.hiddenGames.includes(a.game) &&
(a.intent.events || Object.values(a.routines).some((r) => r.followed)),
);
), prefs.gameOrder, (game) => meta(game).name);
const ordered = orderGames(
[...new Set(accounts.map((a) => a.game))],
prefs.gameOrder,
@@ -1,4 +1,5 @@
import { useGameMeta } from "../../state/gameMeta.tsx";
import { orderGameItems } from "../../state/gameOrder.ts";
import { serverSuffix } from "../../state/reader/servers.ts";
import type { CSSProperties } from "react";
import { useEffect, useRef, useState } from "react";
@@ -193,6 +194,11 @@ export function DailyPanel({
now: number;
}) {
const meta = useGameMeta();
rows = orderGameItems(
rows,
Array.isArray(state.prefs.gameOrder) ? state.prefs.gameOrder as string[] : undefined,
(game) => meta(game).name,
);
const total = rows.length;
const done = rows.filter((r) =>
doneToday(r, readerDayKey(now, r.clock)),
@@ -2,6 +2,7 @@ import { routinePeriod } from "./TimelinePage.tsx";
import { manyServers, serverSuffix } from "../../state/reader/servers.ts";
import { formatRemaining } from "../../../shared/time.ts";
import { useGameMeta } from "../../state/gameMeta.tsx";
import { orderGameItems } from "../../state/gameOrder.ts";
import type { CSSProperties } from "react";
import { RoutineHistory } from "./RoutineHistory.tsx";
import { useState } from "react";
@@ -120,14 +121,15 @@ export function RoutinesPage({
setEditing(task);
setDrawer(pane);
};
const accounts = Object.values(state.accounts).filter(
const accounts = orderGameItems(Object.values(state.accounts).filter(
(a) =>
!a.retiredAt &&
!(
Array.isArray(state.prefs.hiddenGames) &&
state.prefs.hiddenGames.includes(a.game)
),
);
), Array.isArray(state.prefs.gameOrder) ? state.prefs.gameOrder as string[] : undefined,
(game) => meta(game).name);
const save = async (draft: TaskDraft, placement: CreatePlacement) => {
const at = new Date().toISOString(),
id = editing ? editing.id : mintTaskId();
+10
View File
@@ -64,6 +64,16 @@ export function orderGames(
return [...placed, ...alphabetical(lanes.filter((id) => !seen.has(id)))];
}
/** Apply the same display order to accounts or tasks, keeping ties stable. */
export function orderGameItems<T extends { game: LaneId }>(
items: readonly T[],
stored: readonly LaneId[] | undefined,
nameOf: (id: LaneId) => string,
): T[] {
const games = orderGames([...new Set(items.map((item) => item.game))], stored, nameOf);
return [...items].sort((a, b) => games.indexOf(a.game) - games.indexOf(b.game));
}
/**
* Move one entry, shifting the rest.
*
+16
View File
@@ -50,6 +50,22 @@ export const KIND_LABEL: Record<Entry["kind"], string> = {
};
export const ENTRIES: Entry[] = [
{
id: "nte-lost-exchange",
date: "2026-11-02",
kind: "games",
title: "NTE's monthly Lost Exchange pulls are in the routine library",
body: "Track 5 Solid Dice, 5 Fabricated Dice and 20 Tri-Keys each month. Fabricated Dice appear when standard-banner pulls are enabled; follow and pin each shop item separately.",
ref: "0c965e3",
},
{
id: "daily-routine-game-order",
date: "2026-11-02",
kind: "fix",
title: "Dailies and routines follow your Game order",
body: "Today's dailies, progress pips, pinned routines and both Routines grouping modes now use your saved Game order, or A–Z when you haven't set one. Each game's servers and tasks stay together.",
ref: "dd5d374",
},
{
id: "genshin-tcg-setup",
date: "2026-11-01",
+36
View File
@@ -551,6 +551,42 @@ export const ROUTINES: readonly Routine[] = [
},
// Neverness to Everness.
{
id: "routine:nte:lost-exchange-solid-dice",
game: "nte",
title: "Lost Exchange: Solid Dice",
period: { unit: "month", startsOn: 1 },
pulls: "premium",
measure: { kind: "amount", target: 5, unit: "dice" },
source: "https://www.icy-veins.com/neverness-to-everness/items/dicelimite",
asOf: "2026-10-21",
checked: "2026-11-02",
note: "Limited-banner pulls bought with Lost Pieces. Confirmed in game, 2026-11-02.",
},
{
id: "routine:nte:lost-exchange-fabricated-dice",
game: "nte",
title: "Lost Exchange: Fabricated Dice",
period: { unit: "month", startsOn: 1 },
pulls: "standard",
measure: { kind: "amount", target: 5, unit: "dice" },
source: "https://www.icy-veins.com/neverness-to-everness/items/dicenormal",
asOf: "2026-11-02",
checked: "2026-11-02",
note: "Standard-banner pulls bought with Lost Pieces. Confirmed in game, 2026-11-02.",
},
{
id: "routine:nte:lost-exchange-tri-keys",
game: "nte",
title: "Lost Exchange: Tri-Keys",
period: { unit: "month", startsOn: 1 },
pulls: "premium",
measure: { kind: "amount", target: 20, unit: "keys" },
source: "https://www.icy-veins.com/neverness-to-everness/items/weapongacha",
asOf: "2026-10-21",
checked: "2026-11-02",
note: "Weapon-banner pulls bought with Lost Pieces. Confirmed in game, 2026-11-02.",
},
{
id: "routine:nte:season-pass-exp",
game: "nte",
+15 -1
View File
@@ -1,8 +1,22 @@
import { describe, expect, test } from "bun:test";
import { moveGame, orderGames } from "../src/client/state/gameOrder.ts";
import { moveGame, orderGames, orderGameItems } from "../src/client/state/gameOrder.ts";
import { metaFor } from "../src/shared/games.ts";
import type { LaneId } from "../src/shared/custom.ts";
test("ordering tasks keeps a game's servers and tasks stable without changing its input", () => {
const items = [
{ game: "hsr", id: "first" },
{ game: "genshin", id: "asia-daily" },
{ game: "hsr", id: "second" },
{ game: "genshin", id: "asia-weekly" },
{ game: "genshin", id: "europe-daily" },
];
const original = items.slice();
expect(orderGameItems(items, ["genshin", "hsr"], (g) => metaFor(g, {}).name).map((r) => r.id))
.toEqual(["asia-daily", "asia-weekly", "europe-daily", "first", "second"]);
expect(items).toEqual(original);
});
/**
* The reader's game order.
*
+20
View File
@@ -7,6 +7,26 @@ import { readerDayKey } from "../src/shared/readerClock.ts";
const at = "2026-10-16T12:00:00.000Z";
const now = Date.parse(at);
test.each([
{ order: ["zzz", "genshin", "hsr"], expected: ["ZZZ", "Genshin", "Star Rail"] },
{ order: undefined, expected: ["Genshin", "Star Rail", "ZZZ"] },
{ order: ["zzz"], expected: ["ZZZ", "Genshin", "Star Rail"] },
])("dailies and progress pips follow game order $order", ({ order, expected }) => {
let id = 0;
const state = migrateLegacy({}, ["hsr", "genshin", "zzz"], at, () => `account:${++id}`);
state.prefs.gameOrder = order;
const rows = Object.values(state.accounts).flatMap((account) =>
routineRows(state, account.id, now).filter((r) => r.id === `dailies:${account.game}`),
);
const html = render(<DailyPanel rows={rows} state={state} now={now} onAction={async () => ({ ok: true })} />);
expect([...html.matchAll(/<strong>(.*?)<\/strong>/g)].map((m) => m[1])).toEqual([...expected]);
const pips = [...html.matchAll(/<i[^>]*style="([^"]*)"/g)].map((m) => m[1]);
const rowHues = [...html.matchAll(/class="daily-row daily-hit"[^>]*style="([^"]*)"/g)].map((m) => m[1]);
expect(pips).toHaveLength(3);
expect(pips).toEqual(rowHues);
});
function setup() {
const state = migrateLegacy({}, ["genshin"], at, () => "account:daily");
state.accounts["account:daily"]!.routines["dailies:genshin"] = {
+11
View File
@@ -45,6 +45,17 @@ test("bulk pins combine changes per account and preserve the other placement", (
});
import { RoutinesPage } from "../src/client/components/redesign/RoutinesPage.tsx";
test.each(["game", "reset"])("routines follow Game order when grouped by %s", (group) => {
const at = "2026-10-16T12:00:00.000Z";
let id = 0;
const state = migrateLegacy({}, ["hsr", "genshin", "zzz"], at, () => `account:${++id}`);
state.prefs.gameOrder = ["zzz", "genshin", "hsr"];
state.prefs.routineGroup = group;
const html = render(<RoutinesPage state={state} now={Date.parse(at)} onAction={async () => ({ ok: true })} />);
const cells = [...html.matchAll(/class="gcell"><span>([^<]*)/g)].map((m) => m[1]);
expect(cells).toEqual(["ZZZ", "Genshin", "Star Rail"]);
});
test("the routines table shows progress as text with an edit affordance, not inputs", () => {
const at = "2026-10-16T12:00:00.000Z";
const state = migrateLegacy({}, ["genshin"], at, () => "account:table");
+8
View File
@@ -113,6 +113,9 @@ describe("ROUTINES", () => {
["routine:wuwa:shop-lustrous-tide", "standard"],
]);
expect(shape("nte")).toEqual([
["Lost Exchange: Solid Dice", { unit: "month", startsOn: 1 }, { kind: "amount", target: 5, unit: "dice" }],
["Lost Exchange: Fabricated Dice", { unit: "month", startsOn: 1 }, { kind: "amount", target: 5, unit: "dice" }],
["Lost Exchange: Tri-Keys", { unit: "month", startsOn: 1 }, { kind: "amount", target: 20, unit: "keys" }],
["Season Pass weekly EXP", week, { kind: "amount", target: 12000, unit: "EXP" }],
["Anomaly Pilgrimage", week, { kind: "amount", target: 3, unit: "claims" }],
["City Stamina", week, { kind: "amount", target: 700, unit: "stamina" }],
@@ -121,6 +124,11 @@ describe("ROUTINES", () => {
["Beyond the Rails", { unit: "cycle", days: 14, from: "2026-10-22" }, { kind: "days", required: 1 }],
["Tycoon Incentive Fund", { unit: "cycle", days: 14, from: "2026-10-26" }, { kind: "amount", target: 1000000, unit: "fons" }],
]);
expect(routinesFor("nte").filter((r) => r.pulls !== undefined).map((r) => [r.id, r.pulls])).toEqual([
["routine:nte:lost-exchange-solid-dice", "premium"],
["routine:nte:lost-exchange-fabricated-dice", "standard"],
["routine:nte:lost-exchange-tri-keys", "premium"],
]);
expect(shape("nikki")).toEqual([
["Mira Journey weekly EXP", week, { kind: "amount", target: 8000, unit: "Journey EXP" }],
["Realm of Breakthrough", week, { kind: "amount", target: 2, unit: "trials" }],