feat: add Arknights: Endfield, and stop passing off requirements as summaries

Endfield was previously written off as undatable. That was wrong: a first
pass only inspected its Duration rows (all "Permanently Available") and its
year-less version grid, and missed an "Event | Schedule & Summary" table
whose cells read "Period: 08/09/26 - 08/30/26 During the event...". Two real
events, with a year.

Adds MM/DD/YY range parsing for that shape. Month-first ordering is not
assumed lightly — Endfield's own version grid reads 01/22, 04/17, 07/16 for
versions 1.0, 1.2 and 1.4, chronological only if the month comes first.

Also stops presenting unlock conditions as descriptions. Several templates
put "Reach Union Level 8" where a blurb would go; filling the summary slot
with text that never says what the event is is worse than leaving it empty.
Wuthering Waves correctly drops to zero summaries as a result.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:07:39 +02:00
co-authored by Claude Opus 5
parent 65f1660718
commit ad45b52c84
11 changed files with 237 additions and 19 deletions
+3 -2
View File
@@ -99,8 +99,9 @@ These come from how gacha games actually schedule things, and they cause most bu
- **Game8 has no single template.** Three shapes are known so far, and a page may mix them:
1. Label/value detail tables (`Event Start` / `Event End`) — Genshin.
2. Column tables (`Event | Duration | Event Details | Rewards`) — NTE.
3. Image-grid schedules with a bare `MM/DD` and no end — Arknights: Endfield. **Unsupportable**;
yields nothing by design.
3. Image-grid schedules with a bare `MM/DD` and no end — unsupportable, yields nothing by design.
4. Combined cells that fold label, range and blurb together
(`Period: 08/09/26 - 08/30/26 During the event...`) — Arknights: Endfield.
Before assuming a new Game8 page will work, dump its structure and check which shape it uses.
- **Silent drops are the dangerous failure.** A date format the parser does not recognise makes
events vanish with no error. Abbreviated months (`Apr. 29 - May 13, 2026`) are supported for
+6 -5
View File
@@ -16,7 +16,7 @@ server, database and scheduler are specified but not built.
| Piece | State |
|---|---|
| Event schema, date parsing, Game8 parser | Built, tested |
| Five game sources (Genshin, Star Rail, Wuthering Waves, ZZZ, NTE) | Built, tested |
| Six game sources | Built, tested |
| Cross-source merge and conflict detection | Built, tested |
| Web interface, offline support | Built |
| Bun server, SQLite, refresh scheduler, review queue | Specified in `docs/`, not built |
@@ -82,15 +82,16 @@ date makes you miss content. Given the choice, this ships nothing rather than a
| Wuthering Waves | Game8 | 10 |
| Zenless Zone Zero | Game8 | 12 |
| Neverness to Everness | Game8 | 13 |
| Arknights: Endfield | — | No usable source, see below |
| Arknights: Endfield | Game8 | 2 |
Game8 uses a different page template for almost every game — label/value detail tables, column
tables, rowspan Start/End pairs — and four different date formats between them. One parser handles
all of it; each game costs a registry entry.
**Endfield has no adapter on purpose.** Its Game8 page carries no usable dates: every duration reads
"Permanently Available", and the schedule is an image grid showing `07/16` with no year and no end
date. Supporting it would mean inventing both. It needs a different source.
Endfield yields only two events because most of its page genuinely has no dates every `Duration`
row reads "Permanently Available" and its version grid shows `07/16` with no year. The two dated
events hide in a combined cell (`Period: 08/09/26 - 08/30/26 During the event...`), which is where
the `MM/DD/YY` parser earns its place.
Arknights is defined in the schema and awaiting a source.
+6 -3
View File
@@ -94,10 +94,13 @@ Game8 uses at least three page templates and a game's page may use any of them:
2. **Column tables**`Event | Duration | Event Details | Rewards`, one row per event, under a
section heading. *(Neverness to Everness)*
3. **Image-grid schedules** — a bare `MM/DD`, no year, no end date. **Unsupportable.**
*(Arknights: Endfield)*
4. **Combined cells** — one cell holding label, range and blurb
(`Period: 08/09/26 - 08/30/26 During the event...`). *(Arknights: Endfield)*
Shapes 1 and 2 are handled. Before assuming a new Game8 page will work, dump its heading/table
structure and check which shape it uses.
Shapes 1, 2 and 4 are handled. Before assuming a new Game8 page will work, dump its heading/table
structure and check which shape it uses — and check **every** table, not just the obvious one.
Endfield was written off as undatable on a first pass that only inspected its `Duration` rows; its
two real events were in a table further down.
## Stage 1 — fetch
@@ -0,0 +1,44 @@
[
{
"id": "endfield:bedazzling-dawnstar-sign-in:2026-08-09",
"game": "endfield",
"title": "Bedazzling Dawnstar Sign-In",
"type": "login",
"summary": "Sign-in to get extra pulls for Liino!",
"startsAt": "2026-08-09T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-30T00:00:00.000Z",
"endPrecision": "day",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
"sourceId": "endfield-game8-events",
"status": "published",
"confidence": 0.85,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "endfield:the-rooted-realm:2026-08-09",
"game": "endfield",
"title": "The Rooted Realm",
"type": "other",
"summary": "During the event, gather Aurylene Fragments and complete Realm Quests to claim generous rewards.",
"startsAt": "2026-08-09T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-30T00:00:00.000Z",
"endPrecision": "day",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
"sourceId": "endfield-game8-events",
"status": "published",
"confidence": 0.85,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
}
]
@@ -3,7 +3,7 @@
window.gon={};gon.rails_env="production";gon.url="https://game8.co/games/Arknights-Endfield/archives/535443";gon.cookie_consent=false;gon.device="pc";gon.is_signed_in=false;gon.beacon_track_path="https://tracking.game8.co/track_access";gon.beacon_click_path="https://tracking.game8.co/track_nier";gon.beacon_load_path="https://tracking.game8.co/track_mario";gon.beacon_event_path="https://tracking.game8.co/track_event";gon.beacon_feedback_path="https://tracking.game8.co/track_feedback";gon.game_id=1405;gon.game_name="Arknights: Endfield";gon.category_ids=[1024,1025];gon.comsumer_game=0;gon.is_sales=false;gon.is_focus=false;gon.game8=1;gon.archive_id=535443;gon.title="All Events and Schedule | Arknights: EndfieldGame8";gon.is_bbs=false;gon.ballots=[];gon.prohibited_words=[];gon.archive_category_id=null;
//]]>
</script><meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="28OYFw9QpyYYT6fN-CUUqoIwTiHQvSGaCd_3-A5OpXo_CdBmcD1iwvgpB8cZkLp0L2_mUow5TIuNdy_5cTMpgw" /><script src="https://assets.game8.co/assets/shared/head/themeInitializer-4ed3610935d8af30d7abe4195b60088bc3a42b2e60d1d38b7429b06d55ba4e75.js"></script><script src="https://assets.game8.co/assets/pc/head/application-6aa84feca56673cde68236096bac9eaece433264afbdee2602b42c1a8b93bec1.js"></script><link rel="stylesheet" href="https://assets.game8.co/assets/pc/new/application-5cf0e64fb9e0ca40ef1c4af80b631841e7514032c2194ecbcaaca5e533a24bc5.css" media="all" /><link crossorigin="" href="https://ads.adthrive.com" rel="preconnect" /><link href="https://ads.adthrive.com" rel="dns-prefetch" /><!-- Raptive Head Tag (immediate load: CO-3303 full-site rollout) -->
<meta name="csrf-token" content="JtSWVQvSbW4Iu4g4yYQArjwEDZaK6VwHyxibIioXiS-1thAkxdTTJKcZvnSvIxq2GsG6wnCsk0QtptaAK-Ud7w" /><script src="https://assets.game8.co/assets/shared/head/themeInitializer-4ed3610935d8af30d7abe4195b60088bc3a42b2e60d1d38b7429b06d55ba4e75.js"></script><script src="https://assets.game8.co/assets/pc/head/application-6aa84feca56673cde68236096bac9eaece433264afbdee2602b42c1a8b93bec1.js"></script><link rel="stylesheet" href="https://assets.game8.co/assets/pc/new/application-5cf0e64fb9e0ca40ef1c4af80b631841e7514032c2194ecbcaaca5e533a24bc5.css" media="all" /><link crossorigin="" href="https://ads.adthrive.com" rel="preconnect" /><link href="https://ads.adthrive.com" rel="dns-prefetch" /><!-- Raptive Head Tag (immediate load: CO-3303 full-site rollout) -->
<script data-no-optimize="1" data-cfasync="false">
(function(w, d) {
w.adthrive = w.adthrive || {};
@@ -735,7 +735,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
<td></td>
</tr>
</table>
</div><div class="track_mario" data-track-mario-keyword="comment_top" data-track-mario-value="535443"></div><div class="c-container"><h2 class="a-header--2">Comment</h2><div class="c-row"><div class="c-commentItem c-container--padding-sp"><div class="c-commentItem__header"><span class="c-commentItem__userName"><span>3</span></span>&nbsp;<span class="c-commentItem__userIcon"><img alt="Дмитрий 293" class="c-commentItem__avatar" src="https://lh3.googleusercontent.com/a/ACg8ocJosnaaE_3BuKO96pSvBTaUb2XfFRHlMwcCy_bExQ6qqlBQUg=s96-c" /></span>&nbsp;<span class="c-commentItem__userName"><span>Дмитрий 293</span></span>&nbsp;<span class="c-commentItem__date">29 days</span></div><div class="c-commentItem__body"><p>WFFC0BV9H9REWPB3</p></div></div><div class="c-commentItem c-container--padding-sp"><div class="c-commentItem__header"><span class="c-commentItem__userName"><span>2</span></span>&nbsp;<span class="c-commentItem__userIcon"><img alt="Walkthrough Team" class="c-commentItem__avatar" src="https://lh3.googleusercontent.com/a/AGNmyxbWsEInweoi4lqKltJv7wN8J9pIOxyOWODG4CUx=s50-mo" /></span>&nbsp;<span class="c-commentItem__userName"><span>Walkthrough Team</span><span class="comment__official">@Game8</span></span>&nbsp;<span class="c-commentItem__date">7 months</span></div><div class="c-commentItem__body"><p><button class='js-open-reply-modal c-commentItem__anchor c-commentItem__btn' data-micromodal-trigger='js-reply-modal' data-anchor='{"id":535443,"response_number":2,"reply_num":"1"}')'> >>1 </button> Hi! We have the SKPORT Community Sign-In Event in our Daily Sign-In Event Guide: <a href='https://game8.co/games/Arknights-Endfield/archives/574950' target='_black' rel='nofollow'>https://game8.co/games/Arknights-Endfield/archives/574950</a>
</div><div class="track_mario" data-track-mario-keyword="comment_top" data-track-mario-value="535443"></div><div class="c-container"><h2 class="a-header--2">Comment</h2><div class="c-row"><div class="c-commentItem c-container--padding-sp"><div class="c-commentItem__header"><span class="c-commentItem__userName"><span>3</span></span>&nbsp;<span class="c-commentItem__userIcon"><img alt="Дмитрий 293" class="c-commentItem__avatar" src="https://lh3.googleusercontent.com/a/ACg8ocJosnaaE_3BuKO96pSvBTaUb2XfFRHlMwcCy_bExQ6qqlBQUg=s96-c" /></span>&nbsp;<span class="c-commentItem__userName"><span>Дмитрий 293</span></span>&nbsp;<span class="c-commentItem__date">30 days</span></div><div class="c-commentItem__body"><p>WFFC0BV9H9REWPB3</p></div></div><div class="c-commentItem c-container--padding-sp"><div class="c-commentItem__header"><span class="c-commentItem__userName"><span>2</span></span>&nbsp;<span class="c-commentItem__userIcon"><img alt="Walkthrough Team" class="c-commentItem__avatar" src="https://lh3.googleusercontent.com/a/AGNmyxbWsEInweoi4lqKltJv7wN8J9pIOxyOWODG4CUx=s50-mo" /></span>&nbsp;<span class="c-commentItem__userName"><span>Walkthrough Team</span><span class="comment__official">@Game8</span></span>&nbsp;<span class="c-commentItem__date">7 months</span></div><div class="c-commentItem__body"><p><button class='js-open-reply-modal c-commentItem__anchor c-commentItem__btn' data-micromodal-trigger='js-reply-modal' data-anchor='{"id":535443,"response_number":2,"reply_num":"1"}')'> >>1 </button> Hi! We have the SKPORT Community Sign-In Event in our Daily Sign-In Event Guide: <a href='https://game8.co/games/Arknights-Endfield/archives/574950' target='_black' rel='nofollow'>https://game8.co/games/Arknights-Endfield/archives/574950</a>
We&#39;ll feature more events as they release, so make sure to sign up and bookmark the page to be notified of any updates. Thanks!</p></div></div></div><div class="c-row"><a class="a-btn track_nier" data-track-nier-keyword="archive.comment_more_link.click" data-track-nier-value="abtest_pc_1" href="https://game8.co/games/Arknights-Endfield/archives/535443/comments">See more comments (3 Comments)</a></div></div><div class="c-container c-container--top20"><div class="c-share"><div class="c-share__text">Share:</div><div class="c-share__links"><a class="c-share__link c-share__link--fb track_nier" data-track-nier-keyword="share_btn_click" data-track-nier-value="facebook" href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fgame8.co%2Fgames%2FArknights-Endfield%2Farchives%2F535443" target="_blank"><i class="icon-facebook"></i></a><a class="c-share__link c-share__link--tw track_nier" data-track-nier-keyword="share_btn_click" data-track-nier-value="twitter" href="https://twitter.com/share?url=https%3A%2F%2Fgame8.co%2Fgames%2FArknights-Endfield%2Farchives%2F535443&amp;text=All%20Events%20and%20Schedule%20%7C%20Arknights%3A%20Endfield%20%EF%BD%9CGame8%20%40Endfield_Game8&amp;hashtags=ArknightsEndfield,Arknights,Endfield" target="_blank"><i class="icon-twitter"></i></a><a class="c-share__link c-share__link--pn track_nier" data-track-nier-keyword="share_btn_click" data-track-nier-value="pinterest" href="https://www.pinterest.com/pin/create/button/?url=https%3A%2F%2Fgame8.co%2Fgames%2FArknights-Endfield%2Farchives%2F535443" target="_blank"><i class="icon-pinterest"></i></a><a class="c-share__link track_nier" data-track-nier-keyword="share_btn_click" data-track-nier-value="reddit" href="https://www.reddit.com/submit?url=https%3A%2F%2Fgame8.co%2Fgames%2FArknights-Endfield%2Farchives%2F535443" target="_blank"><img class="c-share__link--image" alt="share on reddit" src="https://assets.game8.co/assets/reddit-logo-8613ebbbb7ea55b1958b6503f2a6f55846aac8a0153029bf9bdba8fc8dc085a1.png" /></a><a class="c-share__link track_nier" data-track-nier-keyword="share_btn_click" data-track-nier-value="whatsapp" href="https://api.whatsapp.com/send?text=https%3A%2F%2Fgame8.co%2Fgames%2FArknights-Endfield%2Farchives%2F535443" target="_blank"><img class="c-share__link--image" alt="share on whatsapp" src="https://assets.game8.co/assets/whatsapp-logo-7088e96c6c862b4db7eab40705ab8e8fde278cfc2db6e9318402445b0b53c68d.png" /></a><a class="c-share__link track_nier" data-track-nier-keyword="share_btn_click" data-track-nier-value="messenger" href="https://m.me/?text=https%3A%2F%2Fgame8.co%2Fgames%2FArknights-Endfield%2Farchives%2F535443" target="_blank"><img class="c-share__link--image" alt="share on messenger" src="https://assets.game8.co/assets/messenger-logo-c32947f5cf72fcb1fdad369d9c0969d1f6e1258f05edebd235c8815b3f602f22.png" /></a></div></div></div><div class="c-container"><h2 class="a-header--2">Author</h2><div class="p-article__author" itemscope="" itemtype="https://schema.org/author"><div class="p-article__author__item"><div class="p-article__author__thumb"><img data-src="https://img.game8.co/4174951/db39cd9c943e563398521460c3d83003.png/original" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><div class="p-article__author__text"><a class="track_nier p-archiveHeader__authorLink" data-track-nier-keyword="click_author_link" itemprop="url" href="https://x.com/Endfield_Game8"><div class="p-authorListItem__title archive" itemprop="name">Arknights: Endfield Walkthrough Team</div><div class="p-authorListItem__lead archive" itemprop="description">This article was created by Game8&#39;s elite team of writers and gamers.</div></a></div></div><a class="track_nier p-article__author__xLink" target="_blank" rel="noopener noreferrer" data-track-nier-keyword="click_author_x_link" href="https://x.com/Endfield_Game8"><span class="p-article__author__xLink__icon"><svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" fill="currentColor"></path>
</svg>
@@ -1492,14 +1492,14 @@ We&#39;ll feature more events as they release, so make sure to sign up and bookm
L22,8.3L20.6,10C20.6,10,22,11.7,22,11.7z M19,10c0-3.9-3.1-7-7-7s-7,3.1-7,7s3.1,7,7,7S19,13.9,19,10z M17.1,20.3l2.4,3.7l1.1-2H23
l-2.7-4.1C19.4,18.9,18.3,19.7,17.1,20.3z M3.7,17.9L1,22h2.3l1.1,2l2.4-3.7C5.7,19.7,4.6,18.9,3.7,17.9z"></path>
</svg>
<h2 class="c-heading__title">Rankings</h2></div><div class="c-tabContainer"><div class="c-tabs"><div class="c-tab"><button class="c-tab__link is-active track_nier" data-track-nier-keyword="ranking_tab_a_1_pc" type="button">Popular Articles</button></div><div class="c-tab"><button class="c-tab__link track_nier" data-track-nier-keyword="ranking_tab_a_2_pc" type="button">Message Board</button></div></div><div class="c-tabPanels"><div class="c-tabPanel is-active"><ul class="c-tabList"><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--1">1</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Liino Best Build and Weapons" href="https://game8.co/games/Arknights-Endfield/archives/608706">Liino Best Build and Weapons</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--2">2</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Cryostatic Connection Stage 6 Guide for Version 1.2" href="https://game8.co/games/Arknights-Endfield/archives/598853">Cryostatic Connection Stage 6 Gu...</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--3">3</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Umbral Monument Endgame Guide" href="https://game8.co/games/Arknights-Endfield/archives/579874">Umbral Monument Endgame Guide</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--4">4</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Insider Threat Mission Guide" href="https://game8.co/games/Arknights-Endfield/archives/577267">Insider Threat Mission Guide</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--5">5</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="All Voice Actors List" href="https://game8.co/games/Arknights-Endfield/archives/575423">All Voice Actors List</a></li><li class="c-tabListItem"><a class="c-tabListItem__link c-tabListItem__more track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="see more" href="https://game8.co/games/Arknights-Endfield/ranking">See more</a></li></ul></div><div class="c-tabPanel"><ul class="c-tabList"><li class="c-tabListItem"><div class="c-tabListItem__link">We could not find the message board you were looking for.</div></li></ul></div></div></div></div><div class="js-recruit-banner-container" data-api-endpoint="/api/geo_location/country" data-cache-key="sidebar_recruit_ph_v1" data-nosnippet="" data-target-country="PH" style="display: none;"><div class="c-sideContainer"><a target="_blank" rel="nofollow noopener" class="track_nier" data-track-nier-keyword="sidebar_recruit" data-track-nier-value="ph_recruit_1405" href="https://forms.gle/tBc2yezUA5EULGgU9"><img alt="Game8 Recruitment" style="width: 100%; height: auto; display: block;" loading="lazy" src="https://assets.game8.co/assets/new/recruit/1405-5d4dee1775af334e72c712ac52969b41b0e50162ba63236902ea41cd0f91eeb5.png" /></a></div></div><div class="c-sideContainer"></div><div class="c-sideContainer"><div class="c-heading c-heading--orange"><svg viewbox="0 0 24 24" class="c-heading__icon">
<h2 class="c-heading__title">Rankings</h2></div><div class="c-tabContainer"><div class="c-tabs"><div class="c-tab"><button class="c-tab__link is-active track_nier" data-track-nier-keyword="ranking_tab_a_1_pc" type="button">Popular Articles</button></div><div class="c-tab"><button class="c-tab__link track_nier" data-track-nier-keyword="ranking_tab_a_2_pc" type="button">Message Board</button></div></div><div class="c-tabPanels"><div class="c-tabPanel is-active"><ul class="c-tabList"><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--1">1</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Perlica Best Build and Weapons" href="https://game8.co/games/Arknights-Endfield/archives/523675">Perlica Best Build and Weapons</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--2">2</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Camille Best Builds and Weapons" href="https://game8.co/games/Arknights-Endfield/archives/601890">Camille Best Builds and Weapons</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--3">3</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Beginner&#39;s Chartered Tribute Guide" href="https://game8.co/games/Arknights-Endfield/archives/585225">Beginner&#39;s Chartered Tribute Guide</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--4">4</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="All Arknights: Endfield Builds" href="https://game8.co/games/Arknights-Endfield/archives/575720">All Arknights: Endfield Builds</a></li><li class="c-tabListItem"><span class="c-tabListItem__rank c-tabListItem__rank--5">5</span><a class="c-tabListItem__link track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="Mi Fu Best Builds and Weapons" href="https://game8.co/games/Arknights-Endfield/archives/574944">Mi Fu Best Builds and Weapons</a></li><li class="c-tabListItem"><a class="c-tabListItem__link c-tabListItem__more track_nier" data-track-nier-keyword="popular_articles_pc" data-track-nier-value="see more" href="https://game8.co/games/Arknights-Endfield/ranking">See more</a></li></ul></div><div class="c-tabPanel"><ul class="c-tabList"><li class="c-tabListItem"><div class="c-tabListItem__link">We could not find the message board you were looking for.</div></li></ul></div></div></div></div><div class="js-recruit-banner-container" data-api-endpoint="/api/geo_location/country" data-cache-key="sidebar_recruit_ph_v1" data-nosnippet="" data-target-country="PH" style="display: none;"><div class="c-sideContainer"><a target="_blank" rel="nofollow noopener" class="track_nier" data-track-nier-keyword="sidebar_recruit" data-track-nier-value="ph_recruit_1405" href="https://forms.gle/tBc2yezUA5EULGgU9"><img alt="Game8 Recruitment" style="width: 100%; height: auto; display: block;" loading="lazy" src="https://assets.game8.co/assets/new/recruit/1405-5d4dee1775af334e72c712ac52969b41b0e50162ba63236902ea41cd0f91eeb5.png" /></a></div></div><div class="c-sideContainer"></div><div class="c-sideContainer"><div class="c-heading c-heading--orange"><svg viewbox="0 0 24 24" class="c-heading__icon">
<path d="M19,2c1.7,0,3,1.3,3,3v14c0,1.7-1.3,3-3,3H5c-1.7,0-3-1.3-3-3V5c0-1.7,1.3-3,3-3H19z M19,0H5C2.2,0,0,2.2,0,5
v14c0,2.8,2.2,5,5,5h14c2.8,0,5-2.2,5-5V5C24,2.2,21.8,0,19,0z M10,9H8v2H6V9H4V7h2V5h2v2h2V9z M17,5c-0.6,0-1,0.4-1,1s0.4,1,1,1
s1-0.4,1-1S17.6,5,17,5z M17,9c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S17.6,9,17,9z M15,7c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1
S15.6,7,15,7z M19,7c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S19.6,7,19,7z M9,15c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S10.1,15,9,15z
M15,15c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S16.1,15,15,15z"></path>
</svg>
<h2 class="c-heading__title">Gaming News</h2></div><div aria-hidden="true" class="js-geo-ad-container" data-api-endpoint="/api/geo_location/country" data-cache-key="sidebar_geo_ad_id_v1" data-nosnippet="" data-target-country="ID" itemscope="" itemtype="https://schema.org/WPAdBlock" rel="nofollow" style="display: none;"><div class="c-sideArticleList__pickup_article"><a class="c-sideArticleList__pickup_article_img track_nier" target="_blank" rel="nofollow noopener" data-track-nier-keyword="sidebar_ads" data-track-nier-value="side_image_id_advertiser_recruitment" href="https://forms.gle/rF5Dfndtne88sgVv8"><img alt="Game8 Advertiser Recruitment" style="width: 100%; height: auto; display: block;" loading="lazy" src="https://assets.game8.co/assets/new/ads/bn_Advertiser-recruitment_id-d7a3a20e67eb34849139090a72d331e93154ff764cb16fe60b17dd22b0e7e436.jpg" /></a></div></div><div class="js-geo-ad-fallback" style="display: none;"><div class="c-sideArticleList__pickup_article"><a class="c-sideArticleList__pickup_article_img track_nier" data-track-nier-keyword="wiki_sidebar_pickup_article" data-track-nier-value="11454" href="/articles/latest/gamescom-2026-everything-announced"><img alt="Gamescom 2026 | Everything We Know" data-src="https://img.game8.co/4590457/b8feb7aefe2b6c76b0eb0648f494b53f.png/show" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /><p class="c-sideArticleList__pickup_article_title">Gamescom 2026</p></a></div></div><ul class="c-sideArticleList"><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_article_link" data-track-nier-value="11458" href="/articles/reviews/clawed/clawed-review"><div class="c-sideArticleList__item_thumb"><img alt="Clawed Review [First Impressions] | In Danger of Extinction" data-src="https://img.game8.co/4590814/93ebeb870bb254da38dc3b343942f5aa.png/thumb" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><p class="c-sideArticleList__item_title">Clawed Review [First Impressions]</p></a></li><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_article_link" data-track-nier-value="11457" href="/articles/reviews/jojo-s-bizarre-adventure-golden-spirit/jojos-bizarre-adventure-golden-spirit-review"><div class="c-sideArticleList__item_thumb"><img alt="JoJo&#39;s Bizarre Adventure: Golden Spirit Review [First Impressions] | Not Quite a Golden Experience" data-src="https://img.game8.co/4590748/61780b6e89bd0f0e3bff28e340bee8f9.jpeg/thumb" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><p class="c-sideArticleList__item_title">JoJo&#39;s Bizarre Adventure: Golden Spirit Review [First Imp...</p></a></li><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_article_link" data-track-nier-value="11259" href="/articles/reviews/trickcal-chibi-go/trickcal-chibi-go-review"><div class="c-sideArticleList__item_thumb"><img alt="Trickcal: Chibi Go Review | A Cute Gacha With Staying Power" data-src="https://img.game8.co/4559920/2e7f717cab0a218c9603ee8c1d50d0b6.png/thumb" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><p class="c-sideArticleList__item_title">Trickcal: Chibi Go Review</p></a></li><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_top_link" href="/articles"><p class="c-sideArticleList__Top_link">More Gaming News</p></a></li></ul></div><div class="c-sideContainer"><div class="c-heading c-heading--orange"><svg viewbox="0 0 24 24" class="c-heading__icon">
<h2 class="c-heading__title">Gaming News</h2></div><div aria-hidden="true" class="js-geo-ad-container" data-api-endpoint="/api/geo_location/country" data-cache-key="sidebar_geo_ad_id_v1" data-nosnippet="" data-target-country="ID" itemscope="" itemtype="https://schema.org/WPAdBlock" rel="nofollow" style="display: none;"><div class="c-sideArticleList__pickup_article"><a class="c-sideArticleList__pickup_article_img track_nier" target="_blank" rel="nofollow noopener" data-track-nier-keyword="sidebar_ads" data-track-nier-value="side_image_id_advertiser_recruitment" href="https://forms.gle/rF5Dfndtne88sgVv8"><img alt="Game8 Advertiser Recruitment" style="width: 100%; height: auto; display: block;" loading="lazy" src="https://assets.game8.co/assets/new/ads/bn_Advertiser-recruitment_id-d7a3a20e67eb34849139090a72d331e93154ff764cb16fe60b17dd22b0e7e436.jpg" /></a></div></div><div class="js-geo-ad-fallback" style="display: none;"><div class="c-sideArticleList__pickup_article"><a class="c-sideArticleList__pickup_article_img track_nier" data-track-nier-keyword="wiki_sidebar_pickup_article" data-track-nier-value="11445" href="/articles/reviews/steins-gate-re-boot/steins-gate-reboot-review"><img alt="STEINS;GATE RE:BOOT Review | Peak Sci-Fi Strikes Again" data-src="https://img.game8.co/4589550/e39a3bad6c4243d602535429d46e7939.jpeg/show" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /><p class="c-sideArticleList__pickup_article_title">STEINS;GATE RE:BOOT Review</p></a></div></div><ul class="c-sideArticleList"><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_article_link" data-track-nier-value="11458" href="/articles/reviews/clawed/clawed-review"><div class="c-sideArticleList__item_thumb"><img alt="Clawed Review [First Impressions] | In Danger of Extinction" data-src="https://img.game8.co/4590814/93ebeb870bb254da38dc3b343942f5aa.png/thumb" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><p class="c-sideArticleList__item_title">Clawed Review [First Impressions]</p></a></li><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_article_link" data-track-nier-value="11457" href="/articles/reviews/jojo-s-bizarre-adventure-golden-spirit/jojos-bizarre-adventure-golden-spirit-review"><div class="c-sideArticleList__item_thumb"><img alt="JoJo&#39;s Bizarre Adventure: Golden Spirit Review [First Impressions] | Not Quite a Golden Experience" data-src="https://img.game8.co/4590748/61780b6e89bd0f0e3bff28e340bee8f9.jpeg/thumb" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><p class="c-sideArticleList__item_title">JoJo&#39;s Bizarre Adventure: Golden Spirit Review [First Imp...</p></a></li><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_article_link" data-track-nier-value="11259" href="/articles/reviews/trickcal-chibi-go/trickcal-chibi-go-review"><div class="c-sideArticleList__item_thumb"><img alt="Trickcal: Chibi Go Review | A Cute Gacha With Staying Power" data-src="https://img.game8.co/4559920/2e7f717cab0a218c9603ee8c1d50d0b6.png/thumb" class="lazy lazy-fadeIn" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /></div><p class="c-sideArticleList__item_title">Trickcal: Chibi Go Review</p></a></li><li><a class="c-sideArticleList__item track_nier" data-track-nier-keyword="wiki_sidebar_top_link" href="/articles"><p class="c-sideArticleList__Top_link">More Gaming News</p></a></li></ul></div><div class="c-sideContainer"><div class="c-heading c-heading--orange"><svg viewbox="0 0 24 24" class="c-heading__icon">
<path d="M19,2c1.7,0,3,1.3,3,3v14c0,1.7-1.3,3-3,3H5c-1.7,0-3-1.3-3-3V5c0-1.7,1.3-3,3-3H19z M19,0H5C2.2,0,0,2.2,0,5
v14c0,2.8,2.2,5,5,5h14c2.8,0,5-2.2,5-5V5C24,2.2,21.8,0,19,0z M10,9H8v2H6V9H4V7h2V5h2v2h2V9z M17,5c-0.6,0-1,0.4-1,1s0.4,1,1,1
s1-0.4,1-1S17.6,5,17,5z M17,9c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S17.6,9,17,9z M15,7c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1
@@ -235,7 +235,7 @@
"game": "nte",
"title": "Warren Lucky Flip",
"type": "other",
"summary": "Complete daily quests to earn card flips.",
"summary": null,
"startsAt": "2026-08-05T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
+56
View File
@@ -0,0 +1,56 @@
import { useCallback, useEffect, useState } from "react";
import { readJson, writeJson } from "./storage.ts";
export interface Mark {
/** When the reader made this mark. */
at: string;
}
export type Marks = Record<string, Mark>;
/**
* A set of per-event marks, keyed by event ID and persisted locally.
*
* Completions ("I finished this") and ignores ("stop showing me this") are the
* same shape and want the same guarantees, so they share one implementation.
* They stay separate stores because they mean different things: an ignored
* event is hidden, a completed one is dimmed but still counted.
*/
export function useMarkSet(storageKey: string) {
const [marks, setMarks] = useState<Marks>(() =>
readJson<Marks>(storageKey, {}),
);
useEffect(() => {
writeJson(storageKey, marks);
}, [storageKey, marks]);
const toggle = useCallback((id: string) => {
setMarks((prev) => {
if (prev[id] !== undefined) {
const { [id]: _removed, ...rest } = prev;
return rest;
}
return { ...prev, [id]: { at: new Date().toISOString() } };
});
}, []);
const clear = useCallback(() => setMarks({}), []);
/**
* Union merge, keeping the earlier mark. Never removes: nothing else holds a
* copy of these, so a silent deletion would be unrecoverable.
*/
const merge = useCallback((incoming: Marks) => {
setMarks((prev) => {
const next = { ...prev };
for (const [id, value] of Object.entries(incoming)) {
const existing = next[id];
next[id] =
existing === undefined || value.at < existing.at ? value : existing;
}
return next;
});
}, []);
return { marks, toggle, merge, clear };
}
+6
View File
@@ -44,6 +44,12 @@ const SOURCES: SourceSpec[] = [
url: "https://game8.co/games/Zenless-Zone-Zero/archives/457176",
parserId: "game8",
},
{
id: "endfield-game8-events",
game: "endfield",
url: "https://game8.co/games/Arknights-Endfield/archives/535443",
parserId: "game8",
},
{
id: "nte-game8-events",
game: "nte",
+35
View File
@@ -112,6 +112,41 @@ export function parseFullRange(
};
}
/**
* "08/09/26 - 08/30/26" → both instants. Also accepts a four-digit year.
*
* Month-first ordering is not assumed lightly: Game8 writes long dates
* US-style ("August 12, 2026"), and Endfield's own version grid reads 01/22,
* 04/17, 07/16 for versions 1.0, 1.2 and 1.4 — chronological only if the month
* comes first. A day-first reading would make 04/17 an invalid month.
*
* Two-digit years pivot at 70: 26 → 2026. The validator's sanity window
* (start within [now-2y, now+1y]) catches anything this gets wrong.
*/
export function parseShortSlashRange(
input: string,
): { start: ParsedInstant; end: ParsedInstant } | null {
const re =
/(\d{1,2})\/(\d{1,2})\/(\d{2,4})\s*[-–—]\s*(\d{1,2})\/(\d{1,2})\/(\d{2,4})/;
const m = re.exec(input);
if (!m) return null;
const year = (raw: string) => {
const n = Number(raw);
return raw.length <= 2 ? (n < 70 ? 2000 + n : 1900 + n) : n;
};
const n = (i: number) => Number(m[i]);
const startIso = iso(year(m[3] ?? ""), n(1), n(2));
const endIso = iso(year(m[6] ?? ""), n(4), n(5));
if (startIso === null || endIso === null) return null;
return {
start: { iso: startIso, precision: "day" },
end: { iso: endIso, precision: "day" },
};
}
/**
* A range whose start is a real date but whose end is not: "July 10, 2026 -
* Permanent", "Jul. 24, 2026 - End of 4.6", or a lone start date.
+57 -4
View File
@@ -8,6 +8,7 @@ import {
parseMonthDayRange,
parseMonthDayYear,
parseOpenRange,
parseShortSlashRange,
parseSlashDateTimeRange,
type ParsedInstant,
} from "../dates.ts";
@@ -63,7 +64,7 @@ const RANGE_LABEL = /^(availability period|event period|duration|period|dates)$/
/** Column-table header matchers. */
const COL_TITLE = /^(.*\b)?events?$/i;
const COL_RANGE =
/^(event |all )?(duration|dates?|event date|period|availability period|schedule)$/i;
/^(event |all )?(duration|dates?|event date|period|availability period|schedule)( ?& ?summary| and summary)?$/i;
const COL_START = /^start$/i;
const COL_END = /^end$/i;
const COL_SUMMARY = /^(event )?(details?|description|overview)$/i;
@@ -159,7 +160,7 @@ function summaryAfter(nodes: DocNode[], from: number): string | null {
if (node === undefined) break;
if (node.kind !== "p") break;
if (node.isButton || node.text.length === 0) continue;
return node.text.slice(0, 500);
return isRequirementOnly(node.text) ? null : node.text.slice(0, 500);
}
return null;
}
@@ -208,9 +209,15 @@ function readColumnTable(rows: string[][]): Candidate[] {
// year-less summary tables ("08/12 - 08/24") from producing events.
if (range === null) continue;
const summaryCell = summaryIdx === -1 ? undefined : row[summaryIdx];
// Some templates fold the schedule and the blurb into one cell
// ("Period: 08/09/26 - 08/30/26 During the event, gather..."). With no
// separate column, recover the prose from what follows the dates.
const summaryCell =
summaryIdx === -1 ? proseAfterDates(rangeCell) : row[summaryIdx];
const summary =
summaryCell && summaryCell.length > 0 ? summaryCell.slice(0, 500) : null;
summaryCell && summaryCell.length > 0 && !isRequirementOnly(summaryCell)
? summaryCell.slice(0, 500)
: null;
out.push({ title, summary, start: range.start, end: range.end });
}
@@ -262,12 +269,58 @@ function readStartEndTable(headers: string[], rows: string[][]): Candidate[] {
return out;
}
/**
* One date, in any shape this parser understands.
*/
const ONE_DATE =
String.raw`(?:\d{1,2}/\d{1,2}/\d{2,4}|[A-Za-z]+\.?\s+\d{1,2}(?:,\s*\d{4})?)`;
/**
* A leading range, whose end may be a date or a stated non-date such as
* "Permanent" or "End of 4.6". Those words are listed rather than matched
* loosely, so a real description is never mistaken for a range end.
*/
const RANGE_PREFIX = new RegExp(
String.raw`^\s*` +
ONE_DATE +
String.raw`(?:\s*[-\u2013\u2014]\s*(?:` +
ONE_DATE +
String.raw`|permanent|tbd|ongoing|end of [\d.]+))?\s*`,
"i",
);
/**
* Prose that only states how to qualify for an event, not what it is.
*
* Several templates put unlock conditions where a description would go
* ("Reach Union Level 8", "Unlocked by default"). Showing that as the summary
* fills the slot with something that never answers "what is this event?", so
* it is dropped in favour of no summary at all.
*/
function isRequirementOnly(text: string): boolean {
return /^(reach|unlock|unlocks|unlocked|require|requires|required|complete|completing|clear|clearing|obtain|available|becomes available|must |need |finish)\b/i.test(
text.trim(),
);
}
/** Strip a leading label and date range, leaving any description behind it. */
function proseAfterDates(cell: string): string | null {
const rest = cell
.replace(/^\s*(period|duration|schedule|dates?)\s*[:]\s*/i, "")
.replace(RANGE_PREFIX, "")
.trim();
// Too short to be a description — probably leftover punctuation.
if (rest.length < 12) return null;
return isRequirementOnly(rest) ? null : rest;
}
function parseRange(
value: string,
): { start: ParsedInstant; end: ParsedInstant | null } | null {
return (
parseSlashDateTimeRange(value) ??
parseFullRange(value) ??
parseShortSlashRange(value) ??
parseMonthDayRange(value) ??
parseOpenRange(value)
);
+19
View File
@@ -25,6 +25,7 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
{ adapter: adapter("hsr-game8-events"), fixture: "fixtures/hsr/game8-events-2026-08-14" },
{ adapter: adapter("wuwa-game8-events"), fixture: "fixtures/wuwa/game8-events-2026-08-14" },
{ adapter: adapter("zzz-game8-events"), fixture: "fixtures/zzz/game8-events-2026-08-14" },
{ adapter: adapter("endfield-game8-events"), fixture: "fixtures/endfield/game8-events-2026-08-14" },
];
async function runAdapter(adapter: Adapter, fixture: string) {
@@ -205,3 +206,21 @@ describe("new source shapes", () => {
expect(jade?.endsAt).toBe("2026-08-13T00:00:00.000Z");
});
});
describe("endfield", () => {
test("reads MM/DD/YY ranges from a combined schedule cell", async () => {
// This page hides its only dated events in an "Event | Schedule & Summary"
// table, where one cell holds the label, the range and the blurb.
const events = await runAdapter(
adapter("endfield-game8-events"),
"fixtures/endfield/game8-events-2026-08-14",
);
expect(events).toHaveLength(2);
const rooted = events.find((e) => e.title === "The Rooted Realm");
expect(rooted?.startsAt).toBe("2026-08-09T00:00:00.000Z");
expect(rooted?.endsAt).toBe("2026-08-30T00:00:00.000Z");
// The prose after the dates becomes the blurb, without the label.
expect(rooted?.summary).not.toBeNull();
expect(rooted?.summary).not.toMatch(/^Period:/);
});
});