feat(endfield): track AIC Quota Exchange permits by banner rotation

The shop's five permits restock when the character and weapon banners change together, which no week, month or fixed cycle can express: rotations ran 21 days and then 19. A rotation is not a game version, so it gets its own table and caption, and follows the version rule that only a recorded end resets the count. The dates and reset rule are the owner's in-game reading, as no page states them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-11-06 09:14:38 +01:00
1 parent be80e785ca
commit 327f6389cc
16 files changed
+242 -46

No files matched your search

+40 -5
View File
@@ -1,6 +1,8 @@
import { describe, expect, test } from "bun:test";
import { DAY } from "../src/shared/time.ts";
import {
BANNERS,
bannerWindow,
VERSION_DAYS,
VERSIONS,
versionCaption,
@@ -15,11 +17,11 @@ import {
const at = (iso: string) => Date.parse(iso);
describe("VERSIONS", () => {
test("each game's versions are in order and meet end to start", () => {
const games = new Set(VERSIONS.map((v) => v.game));
describe.each([["VERSIONS", VERSIONS], ["BANNERS", BANNERS]] as const)("%s", (_name, table) => {
test("each game's entries are in order and meet end to start", () => {
const games = new Set(table.map((v) => v.game));
for (const game of games) {
const list = VERSIONS.filter((v) => v.game === game);
const list = table.filter((v) => v.game === game);
list.forEach((v, i) => {
expect(Number.isFinite(at(v.start))).toBe(true);
if (v.end !== undefined) expect(at(v.end)).toBeGreaterThan(at(v.start));
@@ -34,7 +36,7 @@ describe("VERSIONS", () => {
});
test("every one cites an https page and the day it was read", () => {
for (const v of VERSIONS) {
for (const v of table) {
expect(v.source.startsWith("https://")).toBe(true);
expect(v.checked).toMatch(/^\d{4}-\d{2}-\d{2}$/);
if (v.note?.toLowerCase().includes("in game") === true)
@@ -136,3 +138,36 @@ describe("versionCaption", () => {
expect(versionCaption(base, at("2026-11-20T00:00:00Z"))).toBe("resets with next version");
});
});
// A shop that restocks when the character and weapon banners change together:
// the same rule as a version, on a table of its own (Endfield, owner, 2026-11-06).
describe("bannerWindow", () => {
test("holds the rotation up to the end the game states", () => {
// In game: "Ending at 11-24 17:00 (UTC-5)", read on 2026-11-06.
expect(bannerWindow("endfield", at("2026-11-24T21:59:00Z"))).toMatchObject({
key: "2026-11-05",
end: at("2026-11-24T22:00:00Z"),
noun: "banner",
});
});
test("past the recorded end it opens the next rotation and only estimates its end", () => {
const w = bannerWindow("endfield", at("2026-11-24T22:00:00Z"))!;
expect(w.key).toBe("2026-11-24");
expect(w.end).toBeUndefined();
expect(w.estimate).toBe(at("2026-12-14T22:00:00Z"));
});
test("a banner table says nothing about the game's version", () => {
expect(versionWindow("endfield", at("2026-11-06T12:00:00Z"))).toBeUndefined();
expect(bannerWindow("wuwa", at("2026-11-06T12:00:00Z"))).toBeUndefined();
});
test("its caption says banner, not version", () => {
const held = at("2026-11-06T12:00:00Z"), open = at("2026-11-25T00:00:00Z");
expect(versionCaption(bannerWindow("endfield", held)!, held)).toBe("resets with next banner · 24 Nov");
expect(versionCaption(bannerWindow("endfield", open)!, open)).toBe(
"resets with next banner · expected ~14 Dec",
);
});
});