Compare commits
3
Commits
2056630f67
...
8af536a410
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8af536a410 | ||
|
|
0c965e32e9 | ||
|
|
dd5d374422 |
+5
-2
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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"] = {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
Reference in New Issue
Block a user