Compare commits

...
Author SHA1 Message Date
ginnoirandClaude Opus 5 4e0c285347 refactor: drop the mojibake workaround from slugifyStem
slugifyStem carried `.replaceAll('ΓÇÖ', "'")`, which patched a corrupted
apostrophe at the slug layer so three notes with mojibake filenames still
produced correct URLs. The underlying filenames are now repaired, so no stem
contains that sequence and the line is dead code that encodes an assumption
which is no longer true.

Proved it is dead rather than assuming: rebuilt and diffed all 411 generated
slugs against the previous build - identical, nothing moved.

The unit test asserted against the corrupted string, so it was testing the
workaround rather than the behaviour. It now uses the real repaired stem,
'A Farfetch’D Story', which exercises the same thing that actually matters -
that U+2019 becomes a hyphen - and still expects 'a-farfetch-d-story'. Added a
case for 'Fused Dimensions' to pin the non-breaking-space folding, since that
name previously carried U+00A0.

Also syncs the catalog for the Fused Dimensions rename. Verified: npm run check
(0 errors, vitest 9/9), npm run build (486 pages), CI=1 npm run test:ui (8/8),
deployed; live 200 on all seven repaired URLs and the h1 renders without NBSP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:03:21 -05:00
ginnoirandClaude Opus 5 e9334f73e8 chore: regenerate slug-keyed manifests after the stem repair
banner-manifest.json and file-manifest.json are build outputs but are tracked,
and both key on the site slug, so the four changed slugs have to be re-emitted
or the next build shows a dirty tree. Content is otherwise identical - only the
four keys and their asset filenames move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:53:05 -05:00
ginnoirandClaude Opus 5 e1302517a5 data: sync catalog - 7 repaired note stems, 4 URL changes
Upstream fix repaired seven mojibake note filenames, which the catalog derives
its stems from. Four hack URLs change as a result:

  /hack/a-il/                -> /hack/anil/
  /hack/fused-adimensions/   -> /hack/fused-dimensions/
  /hack/yu-gi-oh-pok-duel/   -> /hack/yu-gi-oh-pokeduel/
  /hack/recharged-yellow-previously-known-as-pok-mon-yellow-cross/
                             -> ...-as-pokemon-yellow-cross/

The three apostrophe cases keep their existing URLs, because slugifyStem already
carries a replaceAll workaround for the mojibake apostrophe. That workaround and
its unit test are now redundant, since no stem contains the sequence any more,
but both are left in place as harmless defensive code.

Entry count unchanged at 411, so no fixture updates. Verified: npm run check
(0 errors, vitest 9/9), npm run build (486 pages, banners regenerated under the
new slugs), CI=1 npm run test:ui (8/8), deployed. Live 200 on all seven repaired
URLs plus core pages; the four retired slugs now 404 as intended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:52:51 -05:00
ginnoirandClaude Opus 5 b6651b0723 data: sync catalog - 87 summaries cleaned of raw markdown
Summaries render as plain text in a single <p> (ContentSection.astro,
HackCard.astro), so markdown emphasis, [label](url) links, [[wikilinks]],
callouts, headings and bare URLs all showed literally on the site. All 87
affected summaries rewritten as clean prose upstream in the vault.

Counts unchanged at 411, so no fixture updates. Verified: npm run check
(0 errors, vitest 9/9), npm run build (486 pages), CI=1 npm run test:ui (8/8),
plus a scan of all 411 built pages' rendered Summary paragraphs showing zero
markdown residue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:43:18 -05:00
ginnoirandClaude Opus 5 fa92dd25d1 data: sync catalog - repair 11 corrupted version fields
Six versions recovered from evidence (Rutile Ruby 2.2, Clover 1.3.3, Legends -
Kingdom Hearts 3.5, Space World Crystal 4.2, TCG Neo 1.33, Techno Emerald
2.0.6); five set to unknown where no version was ever stated. Rutile Ruby's
download links relabelled to the official build names.

Counts unchanged at 411, so no fixture updates. Verified: npm run check
(0 errors, vitest 9/9), npm run build (486 pages), CI=1 npm run test:ui (8/8),
deployed via scripts/deploy-site.ps1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:25:01 -05:00
ginnoirandClaude Opus 5 c473436882 data: sync catalog - Soulgold v1.0.4.2, Z English v2.13
Version-only refresh of two existing entries; catalog count stays at 411 and no
Playwright fixture counts changed.

Verified: npm run check (0 errors, vitest 9/9), npm run build (486 pages),
CI=1 npm run test:ui (8/8), deployed via scripts/deploy-site.ps1. Live 200 for
/hack/soulgold/ (1.0.4.2) and /hack/z/ (v2.13 (EN)).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:12:46 -05:00
ginnoirandClaude Opus 5 c89dbe7f7d data: sync catalog to 411 hacks + refresh fixture counts
Adds Soulgold, Unchosen, Soulstones, and Soulstones 2: Time Wardens, plus the
new `RPG Maker MV` base route.

Playwright fixtures were two entries stale from the Timeless Diamond /
Spaceless Pearl sync, so they are reset to measured counts rather than deltas:
411 total, GBA 205, PC 86, NDS 52, Expansion 169, New Experience 179, QoL 97,
Complete 348, and the toHaveCount assertions (411 / GBA 205).

Verified: npm run check (0 errors, vitest 9/9), npm run build (486 pages,
banners 411/411), CI=1 npm run test:ui (8/8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:01:51 -05:00
ginnoir 1ac9751073 data: add Timeless Diamond + Spaceless Pearl to catalog 2026-07-20 02:23:52 -05:00
ginnoirandClaude Opus 4.8 70d46933bd data: add Pokemblem to catalog + bump fixture counts
FE8 crossover hack (self-hosted in-game banner). Catalog 404 -> 405;
Playwright fixtures updated (405, GBA 204, New Experience 174, Complete 342).
Built and deployed to romhacks.ginnoir.com.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:25:10 -05:00
ginnoirandClaude Opus 4.8 3562042bc7 data: sync catalog to 404 hacks + bump fixture counts
Adds Hidden Place, Crippling Medical Debt Edition (with self-hosted
boxart banner), and Grimm/Conviction/Miragea's Traveler; restores
Crystal Inheritance to Complete. Playwright fixtures updated (404, PC 83,
New Experience 173, Difficulty 127, QoL 94, Complete 341, GBA 203, Beta 32).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:52:51 -05:00
ginnoirandClaude Opus 4.8 24811d7950 data: add Pokémon Nova to catalog + bump fixture counts
Nova (PhionGames RPG Maker XP / Essentials PC fangame) synced from the
vault export. Catalog 398->399, PC 78->79; banner manifest + Playwright
count fixtures updated. Built and deployed to romhacks.ginnoir.com.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:08:14 -05:00
ginnoir ca1a1a21ac data: refresh romhack archive catalog 2026-07-04 19:01:11 -05:00
ginnoir 26cafb2681 Refresh catalog with complete metadata and Fakemon generation filter.
Updates 396-hack catalog data, banner manifest, and Playwright fixtures for full banner coverage, zero gap counts, and Fakemon roster tags in the Generation sidebar.
2026-06-26 14:21:01 -05:00
ginnoir 29d5817ed0 feat: add fakemon and generation filters to catalog sidebar 2026-06-25 15:57:02 -05:00
ginnoir d3647c3eda feat: update catalog to 396 hacks with generations/fakemon fields 2026-06-25 15:56:55 -05:00
10 changed files with 10656 additions and 3009 deletions

No files matched your search

+10468 -2977
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -20,6 +20,8 @@ const summary = hack.summary || hack.notability || 'No summary has been added ye
data-base={hack.base ?? 'Unknown'}
data-status={hack.status ?? 'Unknown'}
data-types={hack.types.join('|')}
data-fakemon={hack.fakemon ?? 'Unknown'}
data-generations={hack.generations.join('|')}
data-release={hack.releaseDate ?? ''}
data-search={hack.searchText}
style={`--accent: var(--type-${accent.toLowerCase().replaceAll(' ', '-')}, var(--type-unknown));`}
+88 -4
View File
@@ -1,33 +1,40 @@
{
"a-farfetch-d-story": "/assets/banners/a-farfetch-d-story.png",
"a-star-in-the-desert": "/assets/banners/a-star-in-the-desert.webp",
"abstract": "/assets/banners/abstract.gif",
"acceptance": "/assets/banners/acceptance.png",
"acerion-re": "/assets/banners/acerion-re.webp",
"adventure-red-chapter": "/assets/banners/adventure-red-chapter.webp",
"aesthetic-red": "/assets/banners/aesthetic-red.png",
"aether-white-2": "/assets/banners/aether-white-2.webp",
"africanvs": "/assets/banners/africanvs.png",
"alchemist": "/assets/banners/alchemist.png",
"all-in": "/assets/banners/all-in.png",
"altair-sirius-vega": "/assets/banners/altair-sirius-vega.gif",
"ambrosia": "/assets/banners/ambrosia.webp",
"amethyst": "/assets/banners/amethyst.webp",
"ashgray": "/assets/banners/ashgray.webp",
"ashen-frost": "/assets/banners/ashen-frost.png",
"asther-violet": "/assets/banners/asther-violet.png",
"astral-red": "/assets/banners/astral-red.png",
"attack-on-the-space-station": "/assets/banners/attack-on-the-space-station.png",
"autumn-orange": "/assets/banners/autumn-orange.jpg",
"awakening": "/assets/banners/awakening.png",
"azure-platinum": "/assets/banners/azure-platinum.jpg",
"a-il": "/assets/banners/a-il.jpg",
"anil": "/assets/banners/anil.jpg",
"battle-fire": "/assets/banners/battle-fire.png",
"bidoof-version": "/assets/banners/bidoof-version.jpg",
"big-blue": "/assets/banners/big-blue.png",
"binary-sun": "/assets/banners/binary-sun.png",
"birch-s-folly": "/assets/banners/birch-s-folly.jpg",
"bizarre": "/assets/banners/bizarre.webp",
"black-2-silly-edition": "/assets/banners/black-2-silly-edition.png",
"black-boost": "/assets/banners/black-boost.png",
"black-pearl-emerald": "/assets/banners/black-pearl-emerald.png",
"black-and-white-3-genesis": "/assets/banners/black-and-white-3-genesis.webp",
"blaze-black-2-volt-white-2": "/assets/banners/blaze-black-2-volt-white-2.png",
"blaze-black-2-redux": "/assets/banners/blaze-black-2-redux.webp",
"blaze-black": "/assets/banners/blaze-black.png",
"blazing-emerald": "/assets/banners/blazing-emerald.png",
"blinding-white-2": "/assets/banners/blinding-white-2.png",
"bloody-fusion-platinum": "/assets/banners/bloody-fusion-platinum.jpg",
@@ -47,10 +54,15 @@
"colors": "/assets/banners/colors.jpg",
"colosseum-green-adventure": "/assets/banners/colosseum-green-adventure.jpg",
"compass": "/assets/banners/compass.webp",
"concealed": "/assets/banners/concealed.jpg",
"conquest-ultimate": "/assets/banners/conquest-ultimate.jpg",
"consonancia": "/assets/banners/consonancia.png",
"conviction": "/assets/banners/conviction.png",
"cope": "/assets/banners/cope.png",
"coral": "/assets/banners/coral.webp",
"crippling-medical-debt-edition": "/assets/banners/crippling-medical-debt-edition.webp",
"cross-stadium": "/assets/banners/cross-stadium.jpg",
"crown": "/assets/banners/crown.webp",
"crystal-16-bit": "/assets/banners/crystal-16-bit.png",
"crystal-advance-redux": "/assets/banners/crystal-advance-redux.jpg",
"crystal-advance": "/assets/banners/crystal-advance.png",
@@ -58,9 +70,14 @@
"crystal-calm": "/assets/banners/crystal-calm.png",
"crystal-cappuccino-deluxe": "/assets/banners/crystal-cappuccino-deluxe.jpg",
"crystal-clear": "/assets/banners/crystal-clear.jpg",
"crystal-inheritance": "/assets/banners/crystal-inheritance.webp",
"crystal-kaizo": "/assets/banners/crystal-kaizo.png",
"crystal-legacy": "/assets/banners/crystal-legacy.png",
"cubic-crystal": "/assets/banners/cubic-crystal.png",
"dark-knight": "/assets/banners/dark-knight.webp",
"dark-rising-order-destroyed": "/assets/banners/dark-rising-order-destroyed.webp",
"dark-rising-worlds-collide": "/assets/banners/dark-rising-worlds-collide.webp",
"dark-rising-2": "/assets/banners/dark-rising-2.png",
"dark-rising": "/assets/banners/dark-rising.webp",
"dark-violet": "/assets/banners/dark-violet.jpg",
"dark-worship": "/assets/banners/dark-worship.png",
@@ -77,31 +94,44 @@
"elite-redux": "/assets/banners/elite-redux.png",
"elysium": "/assets/banners/elysium.webp",
"emerald-20": "/assets/banners/emerald-20.png",
"emerald-azure": "/assets/banners/emerald-azure.webp",
"emerald-cross": "/assets/banners/emerald-cross.png",
"emerald-extended-cut": "/assets/banners/emerald-extended-cut.jpg",
"emerald-imperium": "/assets/banners/emerald-imperium.jpg",
"emerald-kaizo": "/assets/banners/emerald-kaizo.png",
"emerald-legacy": "/assets/banners/emerald-legacy.webp",
"emerald-party-randomizer-plus": "/assets/banners/emerald-party-randomizer-plus.jpg",
"emerald-rogue-2-0": "/assets/banners/emerald-rogue-2-0.png",
"emerald-rogue": "/assets/banners/emerald-rogue.jpeg",
"emerald-seaglass": "/assets/banners/emerald-seaglass.png",
"emerald-squared": "/assets/banners/emerald-squared.png",
"empyrean": "/assets/banners/empyrean.png",
"energized-emerald": "/assets/banners/energized-emerald.jpg",
"eon-guardians": "/assets/banners/eon-guardians.webp",
"ephemerald": "/assets/banners/ephemerald.png",
"eternal-x-wilting-y": "/assets/banners/eternal-x-wilting-y.gif",
"evolved": "/assets/banners/evolved.jpg",
"exceeded": "/assets/banners/exceeded.jpg",
"factory-adventure": "/assets/banners/factory-adventure.jpg",
"fakemon-fire-red": "/assets/banners/fakemon-fire-red.jpg",
"fire-ash": "/assets/banners/fire-ash.png",
"fire-red-898": "/assets/banners/fire-red-898.jpg",
"fire-red-extended": "/assets/banners/fire-red-extended.webp",
"fire-red-ok-version": "/assets/banners/fire-red-ok-version.jpg",
"fire-of-sky": "/assets/banners/fire-of-sky.webp",
"firered-leafgreen-frlg": "/assets/banners/firered-leafgreen-frlg.img",
"firered-deluxe": "/assets/banners/firered-deluxe.png",
"firered-distorted": "/assets/banners/firered-distorted.jpg",
"firered-multiverse": "/assets/banners/firered-multiverse.webp",
"firered-reignited-leafgreen-regrown": "/assets/banners/firered-reignited-leafgreen-regrown.jpg",
"firered-reignited": "/assets/banners/firered-reignited.jpg",
"firered-rocket-edition": "/assets/banners/firered-rocket-edition.png",
"flora-sky": "/assets/banners/flora-sky.png",
"floral-tempus": "/assets/banners/floral-tempus.jpg",
"following-renegade-platinum": "/assets/banners/following-renegade-platinum.png",
"fool-s-gold": "/assets/banners/fool-s-gold.png",
"frigo-returns": "/assets/banners/frigo-returns.jpg",
"fused-adimensions": "/assets/banners/fused-adimensions.jpg",
"fused-dimensions": "/assets/banners/fused-dimensions.jpg",
"fusion-origins": "/assets/banners/fusion-origins.jpg",
"gs-97-reforged": "/assets/banners/gs-97-reforged.webp",
"gs-chronicles": "/assets/banners/gs-chronicles.png",
@@ -118,11 +148,17 @@
"golden-shield": "/assets/banners/golden-shield.png",
"grand-colosseum": "/assets/banners/grand-colosseum.png",
"greninja-z": "/assets/banners/greninja-z.jpg",
"grimm": "/assets/banners/grimm.png",
"hgss-sevii": "/assets/banners/hgss-sevii.webp",
"heart-and-soul": "/assets/banners/heart-and-soul.png",
"heartgold-generation": "/assets/banners/heartgold-generation.png",
"heartgold-reloaded": "/assets/banners/heartgold-reloaded.jpg",
"hearth": "/assets/banners/hearth.img",
"hero-legacy": "/assets/banners/hero-legacy.png",
"hidden-place": "/assets/banners/hidden-place.webp",
"hisui-red": "/assets/banners/hisui-red.jpg",
"hoenn-white-2": "/assets/banners/hoenn-white-2.jpg",
"hoenn-s-last-wish": "/assets/banners/hoenn-s-last-wish.png",
"hollow-woods": "/assets/banners/hollow-woods.jpg",
"horror-white": "/assets/banners/horror-white.webp",
"hydro-bliss": "/assets/banners/hydro-bliss.png",
@@ -131,9 +167,13 @@
"infernal-legend": "/assets/banners/infernal-legend.jpg",
"infinite-black-2": "/assets/banners/infinite-black-2.jpg",
"infinite-fusion": "/assets/banners/infinite-fusion.jpg",
"infinity": "/assets/banners/infinity.gif",
"inkwell": "/assets/banners/inkwell.png",
"insurgence": "/assets/banners/insurgence.jpg",
"inverse-emerald": "/assets/banners/inverse-emerald.jpg",
"ironmon-red-kaizo": "/assets/banners/ironmon-red-kaizo.jpg",
"itinerant": "/assets/banners/itinerant.gif",
"jam-festival": "/assets/banners/jam-festival.png",
"jet-black": "/assets/banners/jet-black.png",
"johto-legends": "/assets/banners/johto-legends.png",
"kanto-black": "/assets/banners/kanto-black.webp",
@@ -147,13 +187,16 @@
"leaf-green-extreme-randomizer": "/assets/banners/leaf-green-extreme-randomizer.jpg",
"legends-kingdom-hearts": "/assets/banners/legends-kingdom-hearts.jpg",
"legends-x": "/assets/banners/legends-x.webp",
"legends-of-the-arena": "/assets/banners/legends-of-the-arena.png",
"let-s-go-peerless-pikachu-exquisite-eevee": "/assets/banners/let-s-go-peerless-pikachu-exquisite-eevee.jpg",
"let-s-go-lugia": "/assets/banners/let-s-go-lugia.jpg",
"let-s-beat-mew": "/assets/banners/let-s-beat-mew.jpg",
"light-platinum": "/assets/banners/light-platinum.webp",
"liquid-crystal": "/assets/banners/liquid-crystal.jpg",
"low-budget-crystal": "/assets/banners/low-budget-crystal.webp",
"luminescent-platinum": "/assets/banners/luminescent-platinum.webp",
"mastersilver-woofgold": "/assets/banners/mastersilver-woofgold.jpg",
"mega-adventures": "/assets/banners/mega-adventures.png",
"mega-blue": "/assets/banners/mega-blue.png",
"mega-power": "/assets/banners/mega-power.png",
"meissa-moon": "/assets/banners/meissa-moon.jpg",
@@ -161,7 +204,10 @@
"minced-fusions": "/assets/banners/minced-fusions.jpg",
"mineral": "/assets/banners/mineral.jpg",
"miopidgey": "/assets/banners/miopidgey.png",
"miragea-s-traveler": "/assets/banners/miragea-s-traveler.png",
"modern-emerald": "/assets/banners/modern-emerald.webp",
"moemon-mega-firered": "/assets/banners/moemon-mega-firered.png",
"moemon-star-emerald": "/assets/banners/moemon-star-emerald.webp",
"moon-black-2": "/assets/banners/moon-black-2.png",
"moon-emerald-extreme-randomizer": "/assets/banners/moon-emerald-extreme-randomizer.jpg",
"moon-silver": "/assets/banners/moon-silver.png",
@@ -171,51 +217,72 @@
"mystery-dungeon-explorers-of-skies": "/assets/banners/mystery-dungeon-explorers-of-skies.jpg",
"mystery-dungeon-wigglytuff-s-bizarre-adventure": "/assets/banners/mystery-dungeon-wigglytuff-s-bizarre-adventure.png",
"mystery-mail-blooming-chaos": "/assets/banners/mystery-mail-blooming-chaos.png",
"myth-2": "/assets/banners/myth-2.png",
"myth": "/assets/banners/myth.png",
"mythic-silver": "/assets/banners/mythic-silver.webp",
"nameless": "/assets/banners/nameless.png",
"national-history-museum": "/assets/banners/national-history-museum.jpg",
"navy-sapphire": "/assets/banners/navy-sapphire.jpg",
"nebula-1": "/assets/banners/nebula-1.png",
"nebula-2": "/assets/banners/nebula-2.png",
"nebula-3": "/assets/banners/nebula-3.png",
"new-generations": "/assets/banners/new-generations.png",
"nightmare": "/assets/banners/nightmare.webp",
"nova": "/assets/banners/nova.img",
"null": "/assets/banners/null.png",
"obscura": "/assets/banners/obscura.webp",
"obsidian-emerald": "/assets/banners/obsidian-emerald.jpg",
"odyssey": "/assets/banners/odyssey.png",
"omega-paradox-v2": "/assets/banners/omega-paradox-v2.jpg",
"omega-ruby-origins": "/assets/banners/omega-ruby-origins.webp",
"omniversal-emerald": "/assets/banners/omniversal-emerald.jpg",
"onyx-blue": "/assets/banners/onyx-blue.webp",
"opalo": "/assets/banners/opalo.jpg",
"orange": "/assets/banners/orange.jpg",
"order-and-chaos": "/assets/banners/order-and-chaos.png",
"pefect-emerald": "/assets/banners/pefect-emerald.jpg",
"peppermint": "/assets/banners/peppermint.png",
"peridot": "/assets/banners/peridot.png",
"phoenix-red": "/assets/banners/phoenix-red.webp",
"photonic-sun-prismatic-moon": "/assets/banners/photonic-sun-prismatic-moon.png",
"pisces": "/assets/banners/pisces.webp",
"pitch-black-pure-white": "/assets/banners/pitch-black-pure-white.png",
"plastic-pyrite": "/assets/banners/plastic-pyrite.png",
"platinum-kaizo": "/assets/banners/platinum-kaizo.jpg",
"platinum-red": "/assets/banners/platinum-red.jpg",
"platinum-redux": "/assets/banners/platinum-redux.jpg",
"platinum-unlocked": "/assets/banners/platinum-unlocked.jpg",
"pocket-gaiden": "/assets/banners/pocket-gaiden.webp",
"pokemblem": "/assets/banners/pokemblem.png",
"poketale": "/assets/banners/poketale.png",
"poketropolis": "/assets/banners/poketropolis.img",
"pokerogue": "/assets/banners/pokerogue.jpg",
"polished-crystal": "/assets/banners/polished-crystal.png",
"prism": "/assets/banners/prism.png",
"prisme": "/assets/banners/prisme.png",
"project-nova": "/assets/banners/project-nova.img",
"pure-heart-true-soul": "/assets/banners/pure-heart-true-soul.jpg",
"quarantine-crystal": "/assets/banners/quarantine-crystal.png",
"quetzal": "/assets/banners/quetzal.jpg",
"r-o-w-e": "/assets/banners/r-o-w-e.webp",
"re-verse": "/assets/banners/re-verse.jpg",
"radiant-topaz": "/assets/banners/radiant-topaz.png",
"radical-red": "/assets/banners/radical-red.jpg",
"rainbow-gold": "/assets/banners/rainbow-gold.png",
"re-union-dx": "/assets/banners/re-union-dx.jpg",
"re-universe": "/assets/banners/re-universe.webp",
"realidea-system": "/assets/banners/realidea-system.png",
"reborn": "/assets/banners/reborn.png",
"recharged-emerald-rebalanced": "/assets/banners/recharged-emerald-rebalanced.png",
"recharged-emerald": "/assets/banners/recharged-emerald.png",
"recharged-pink": "/assets/banners/recharged-pink.png",
"recharged-yellow-previously-known-as-pok-mon-yellow-cross": "/assets/banners/recharged-yellow-previously-known-as-pok-mon-yellow-cross.webp",
"recharged-yellow-previously-known-as-pokemon-yellow-cross": "/assets/banners/recharged-yellow-previously-known-as-pokemon-yellow-cross.webp",
"recordkeepers": "/assets/banners/recordkeepers.png",
"red-kaizo": "/assets/banners/red-kaizo.jpg",
"rejuvenation": "/assets/banners/rejuvenation.jpg",
"relict": "/assets/banners/relict.img",
"reminiscencia": "/assets/banners/reminiscencia.png",
"renegade-platinum": "/assets/banners/renegade-platinum.png",
"resolute": "/assets/banners/resolute.png",
"revitalus-immortalis": "/assets/banners/revitalus-immortalis.png",
"rising-ruby-sinking-sapphire": "/assets/banners/rising-ruby-sinking-sapphire.webp",
@@ -230,6 +297,8 @@
"rutile-ruby-star-sapphire": "/assets/banners/rutile-ruby-star-sapphire.jpg",
"sacred-gold": "/assets/banners/sacred-gold.jpg",
"saffron": "/assets/banners/saffron.png",
"saiph-2": "/assets/banners/saiph-2.png",
"saiph": "/assets/banners/saiph.png",
"salt-shadow": "/assets/banners/salt-shadow.png",
"scarlet-violet": "/assets/banners/scarlet-violet.jpg",
"scarlet-sapphire": "/assets/banners/scarlet-sapphire.png",
@@ -238,21 +307,29 @@
"serene-crystal": "/assets/banners/serene-crystal.png",
"shadow-legends": "/assets/banners/shadow-legends.png",
"shadow-of-time": "/assets/banners/shadow-of-time.webp",
"shattered-light": "/assets/banners/shattered-light.gif",
"shinju-adventures": "/assets/banners/shinju-adventures.webp",
"shiny-island": "/assets/banners/shiny-island.webp",
"sienna": "/assets/banners/sienna.jpg",
"silver-rival": "/assets/banners/silver-rival.jpg",
"silver-yellow": "/assets/banners/silver-yellow.jpg",
"smogon-emerald": "/assets/banners/smogon-emerald.jpg",
"snakewood-improved": "/assets/banners/snakewood-improved.webp",
"snakewood": "/assets/banners/snakewood.img",
"solar-light-lunar-dark": "/assets/banners/solar-light-lunar-dark.webp",
"solar-platinum": "/assets/banners/solar-platinum.png",
"soothing-silver": "/assets/banners/soothing-silver.png",
"sors-2": "/assets/banners/sors-2.webp",
"sors": "/assets/banners/sors.webp",
"soulgold": "/assets/banners/soulgold.png",
"soulsilver-fusion": "/assets/banners/soulsilver-fusion.jpg",
"soulstones-2-time-wardens": "/assets/banners/soulstones-2-time-wardens.jpg",
"soulstones": "/assets/banners/soulstones.png",
"sour-crystal": "/assets/banners/sour-crystal.jpg",
"sovereign-of-the-skies": "/assets/banners/sovereign-of-the-skies.webp",
"space-world-crystal": "/assets/banners/space-world-crystal.jpg",
"spaceless-pearl": "/assets/banners/spaceless-pearl.webp",
"spades-and-clubs": "/assets/banners/spades-and-clubs.png",
"spirit-of-the-storm": "/assets/banners/spirit-of-the-storm.png",
"star-beasts-asteroid-version-en": "/assets/banners/star-beasts-asteroid-version-en.png",
"star": "/assets/banners/star.webp",
@@ -274,6 +351,7 @@
"team-rocket-jessie-james-edition": "/assets/banners/team-rocket-jessie-james-edition.jpg",
"team-rocket-prepare-for-trouble": "/assets/banners/team-rocket-prepare-for-trouble.webp",
"techno-emerald": "/assets/banners/techno-emerald.jpg",
"tectonic": "/assets/banners/tectonic.png",
"terra": "/assets/banners/terra.jpg",
"the-light-in-our-hearts": "/assets/banners/the-light-in-our-hearts.png",
"the-pit": "/assets/banners/the-pit.png",
@@ -281,12 +359,15 @@
"the-teal-mask": "/assets/banners/the-teal-mask.jpg",
"the-unown-king": "/assets/banners/the-unown-king.gif",
"the-wooper-who-saved-christmas": "/assets/banners/the-wooper-who-saved-christmas.png",
"theta-emerald-ex": "/assets/banners/theta-emerald-ex.img",
"theta-emerald-the-last-dance": "/assets/banners/theta-emerald-the-last-dance.png",
"this-gym-of-mine": "/assets/banners/this-gym-of-mine.png",
"thyme": "/assets/banners/thyme.jpg",
"timeless-diamond": "/assets/banners/timeless-diamond.webp",
"too-many-types-2": "/assets/banners/too-many-types-2.jpg",
"too-many-types": "/assets/banners/too-many-types.png",
"touhoumon-another-world": "/assets/banners/touhoumon-another-world.jpg",
"tourmaline": "/assets/banners/tourmaline.webp",
"transform": "/assets/banners/transform.png",
"two": "/assets/banners/two.webp",
"ultimate-fusion": "/assets/banners/ultimate-fusion.jpg",
@@ -294,17 +375,20 @@
"ultra-something": "/assets/banners/ultra-something.webp",
"unbound": "/assets/banners/unbound.jpg",
"unbreakable-ties": "/assets/banners/unbreakable-ties.jpg",
"unchosen": "/assets/banners/unchosen.png",
"unova-emerald": "/assets/banners/unova-emerald.webp",
"uranium": "/assets/banners/uranium.png",
"utopia": "/assets/banners/utopia.png",
"valiant": "/assets/banners/valiant.jpg",
"vanguard": "/assets/banners/vanguard.png",
"vanishing-point": "/assets/banners/vanishing-point.jpg",
"vega-fairy-ex": "/assets/banners/vega-fairy-ex.jpeg",
"victory-fire": "/assets/banners/victory-fire.jpg",
"vintage-white": "/assets/banners/vintage-white.jpg",
"violette": "/assets/banners/violette.png",
"void": "/assets/banners/void.jpg",
"volt-white-2-redux": "/assets/banners/volt-white-2-redux.jpg",
"volt-white": "/assets/banners/volt-white.webp",
"voyager": "/assets/banners/voyager.gif",
"war-of-masters": "/assets/banners/war-of-masters.webp",
"wasteland": "/assets/banners/wasteland.png",
@@ -321,7 +405,7 @@
"xenoverse": "/assets/banners/xenoverse.jpg",
"yellow-cramorant-edition-en": "/assets/banners/yellow-cramorant-edition-en.png",
"yellow-plus": "/assets/banners/yellow-plus.png",
"yu-gi-oh-pok-duel": "/assets/banners/yu-gi-oh-pok-duel.webp",
"yu-gi-oh-pokeduel": "/assets/banners/yu-gi-oh-pokeduel.webp",
"z": "/assets/banners/z.png",
"bushido": "/assets/banners/bushido.gif",
"clockwork": "/assets/banners/clockwork.webp",
+2 -2
View File
@@ -141,7 +141,7 @@
"kind": "guide"
}
],
"a-il": [
"anil": [
{
"label": "Pokemon Añil V2.09G English.rar",
"url": "https://romhacks-files.ginnoir.com/Pokemon_A_il/Pokemon%20A%C3%B1il%20V2.09G%20English.rar",
@@ -2102,7 +2102,7 @@
"kind": "guide"
}
],
"yu-gi-oh-pok-duel": [
"yu-gi-oh-pokeduel": [
{
"label": "yugipkduel v4.0.6.zip",
"url": "https://romhacks-files.ginnoir.com/Yu-Gi-Oh_Pok_Duel/yugipkduel%20v4.0.6.zip",
+30 -2
View File
@@ -29,6 +29,8 @@ const baseCatalog: RawCatalog = {
base: 'FireRed',
status: 'Complete',
version: '4.1',
generations: ['Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'],
fakemon: 'No',
developer: 'Soupercell',
release_date: '2024',
banner: 'https://romhacks-files.ginnoir.com/_meta/radical/banner.jpg',
@@ -45,7 +47,7 @@ const baseCatalog: RawCatalog = {
links: [{ label: 'Source / info', url: 'https://example.com/radical' }],
},
{
stem: 'A FarfetchΓÇÖD Story',
stem: 'A Farfetch’D Story',
title: 'Pokémon A Farfetch’D Story',
platform: 'PC',
base: null,
@@ -87,7 +89,8 @@ const baseCatalog: RawCatalog = {
describe('catalog normalization', () => {
it('creates deterministic URL-safe slugs from stems', () => {
expect(slugifyStem('Radical Red')).toBe('radical-red');
expect(slugifyStem('A FarfetchΓÇÖD Story')).toBe('a-farfetch-d-story');
expect(slugifyStem('A Farfetch’D Story')).toBe('a-farfetch-d-story');
expect(slugifyStem('Fused Dimensions')).toBe('fused-dimensions');
expect(slugifyStem('Pokémon: Crystal Clear!')).toBe('pokemon-crystal-clear');
});
@@ -104,6 +107,31 @@ describe('catalog normalization', () => {
expect(unknown?.searchText).not.toMatch(/\bnull\b|—/i);
});
it('normalizes roster generations and Fakemon metadata into chips', () => {
const catalog = normalizeCatalog(baseCatalog);
const radical = catalog.hacks.find((hack) => hack.slug === 'radical-red');
expect(radical?.generations).toEqual([
'Gen I',
'Gen II',
'Gen III',
'Gen IV',
'Gen V',
'Gen VI',
'Gen VII',
'Gen VIII',
'Gen IX',
]);
expect(radical?.fakemon).toBe('No');
expect(radical?.chips).toEqual(
expect.arrayContaining([
{ label: 'Roster', value: 'Gen I, Gen II, Gen III, Gen IV, Gen V, Gen VI, Gen VII, Gen VIII, Gen IX', kind: 'generation' },
{ label: 'Fakemon', value: 'No', kind: 'fakemon' },
]),
);
expect(radical?.searchText).toContain('gen ix');
});
it('maps ROM and library paths to internal file-server links', () => {
const catalog = normalizeCatalog(baseCatalog);
const radical = catalog.hacks.find((hack) => hack.slug === 'radical-red');
+8 -1
View File
@@ -56,6 +56,8 @@ export function normalizeHack(raw: RawHack): SiteHack {
const developer = cleanText(raw.developer);
const releaseDate = cleanText(raw.release_date);
const generation = cleanText(raw.generation);
const generations = cleanList(raw.generations);
const fakemon = cleanText(raw.fakemon);
const scrapeDir = cleanText(raw.scrape_dir);
const libraryPath = cleanText(raw.library_path);
const banner = cleanText(raw.banner);
@@ -74,6 +76,8 @@ export function normalizeHack(raw: RawHack): SiteHack {
version,
status,
generation,
generations,
fakemon,
developer,
releaseDate,
banner,
@@ -106,10 +110,12 @@ export function normalizeHack(raw: RawHack): SiteHack {
hack.version,
hack.developer,
hack.releaseDate,
hack.fakemon,
hack.summary,
hack.notability,
hack.story,
...hack.types,
...hack.generations,
...hack.features,
]
.filter(Boolean)
@@ -121,7 +127,6 @@ export function normalizeHack(raw: RawHack): SiteHack {
export function slugifyStem(stem: string): string {
return stem
.replaceAll('ΓÇÖ', "'")
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9]+/g, '-')
@@ -216,6 +221,8 @@ function buildChips(hack: SiteHack): HackChip[] {
addChip(chips, 'status', 'Status', hack.status);
addChip(chips, 'developer', 'Dev', hack.developer);
addChip(chips, 'release_date', 'Release', hack.releaseDate);
addChip(chips, 'generation', 'Roster', hack.generations.join(', '));
addChip(chips, 'fakemon', 'Fakemon', hack.fakemon);
for (const type of hack.types) {
addChip(chips, 'type', 'Type', type);
}
+21
View File
@@ -67,6 +67,27 @@ export function getHacksForFacet(facet: FacetName, value: string, sort: SortKey
return sortHacks(matches, sort);
}
export function getFakemonEntries(): [string, number][] {
const counts: Record<string, number> = {};
for (const hack of catalog.hacks) {
const value = hack.fakemon ?? 'Unknown';
counts[value] = (counts[value] ?? 0) + 1;
}
return Object.entries(counts)
.filter(([value]) => value !== 'Unknown')
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}
export function getGenerationEntries(): [string, number][] {
const counts: Record<string, number> = {};
for (const hack of catalog.hacks) {
for (const gen of hack.generations) {
counts[gen] = (counts[gen] ?? 0) + 1;
}
}
return Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}
export function displayValue(value?: string): string {
return value || 'Unknown';
}
+5 -1
View File
@@ -20,6 +20,8 @@ export interface RawHack {
version?: string | null;
status?: string | null;
generation?: string | null;
generations?: string[] | null;
fakemon?: string | null;
developer?: string | null;
release_date?: string | null;
banner?: string | null;
@@ -46,7 +48,7 @@ export interface ExternalLink {
export interface HackChip {
label: string;
value: string;
kind: 'platform' | 'base' | 'version' | 'status' | 'developer' | 'release_date' | 'type';
kind: 'platform' | 'base' | 'version' | 'status' | 'developer' | 'release_date' | 'generation' | 'fakemon' | 'type';
}
export interface FileLink {
@@ -64,6 +66,8 @@ export interface SiteHack {
version?: string;
status?: string;
generation?: string;
generations: string[];
fakemon?: string;
developer?: string;
releaseDate?: string;
banner?: string;
+10 -1
View File
@@ -2,13 +2,15 @@
import HackCard from '../components/HackCard.astro';
import FilterGroup from '../components/FilterGroup.astro';
import Layout from '../components/Layout.astro';
import { catalog, facetValueSlug, flagshipHacks, getFacetEntries, hacks } from '../lib/site-data';
import { catalog, facetValueSlug, flagshipHacks, getFacetEntries, getFakemonEntries, getGenerationEntries, hacks } from '../lib/site-data';
import type { FacetName } from '../lib/types';
const platforms = getFacetEntries('platform');
const bases = getFacetEntries('base').slice(0, 12);
const types = getFacetEntries('type');
const statuses = getFacetEntries('status');
const fakemonEntries = getFakemonEntries();
const generationEntries = getGenerationEntries();
type StatItem = {
label: string;
@@ -149,6 +151,8 @@ const browseGroups: BrowseGroup[] = [
<FilterGroup title="Base" name="base" entries={bases} />
<FilterGroup title="Type" name="type" entries={types} />
<FilterGroup title="Status" name="status" entries={statuses} />
<FilterGroup title="Fakemon" name="fakemon" entries={fakemonEntries} />
<FilterGroup title="Generation" name="generation" entries={generationEntries} />
</aside>
<div class="catalog-results">
@@ -184,12 +188,17 @@ const browseGroups: BrowseGroup[] = [
const base = selected('base');
const type = selected('type');
const status = selected('status');
const fakemon = selected('fakemon');
const generation = selected('generation');
const cardTypes = card.dataset.types ? card.dataset.types.split('|') : [];
const cardGens = card.dataset.generations ? card.dataset.generations.split('|') : [];
if (query && !card.dataset.search.includes(query)) return false;
if (platform.length && !platform.includes(card.dataset.platform)) return false;
if (base.length && !base.includes(card.dataset.base)) return false;
if (status.length && !status.includes(card.dataset.status)) return false;
if (type.length && !type.some((value) => cardTypes.includes(value))) return false;
if (fakemon.length && !fakemon.includes(card.dataset.fakemon)) return false;
if (generation.length && !generation.some((value) => cardGens.includes(value))) return false;
return true;
}
+22 -21
View File
@@ -11,23 +11,22 @@ test('homepage presents balanced navigation and facet summaries', async ({ page
await expect(primaryNav.getByRole('link', { name: 'Difficulty' })).toHaveCount(0);
const heroStats = page.locator('.hero-stats');
await expect(heroStats).toContainText('368');
await expect(heroStats).toContainText('411');
await expect(heroStats).toContainText('hacks');
await expect(heroStats).toContainText('Platforms');
await expect(heroStats).toContainText('GBA 188');
await expect(heroStats).toContainText('NDS 47');
await expect(heroStats).toContainText('PC 40');
await expect(heroStats).toContainText('GBC 22');
await expect(heroStats).toContainText('GBA 205');
await expect(heroStats).toContainText('PC 86');
await expect(heroStats).toContainText('NDS 52');
await expect(heroStats).toContainText('GBC 37');
await expect(heroStats).toContainText('Types');
await expect(heroStats).toContainText('Expansion 143');
await expect(heroStats).toContainText('New Experience 129');
await expect(heroStats).toContainText('Difficulty 105');
await expect(heroStats).toContainText('QoL 50');
await expect(heroStats).toContainText('Expansion 169');
await expect(heroStats).toContainText('New Experience 179');
await expect(heroStats).toContainText('Difficulty 127');
await expect(heroStats).toContainText('QoL 97');
await expect(heroStats).toContainText('Status');
await expect(heroStats).toContainText('Complete 168');
await expect(heroStats).toContainText('Ongoing 28');
await expect(heroStats).toContainText('Beta 10');
await expect(heroStats).toContainText('Unclassified 161');
await expect(heroStats).toContainText('Complete 348');
await expect(heroStats).toContainText('Ongoing 30');
await expect(heroStats).toContainText('Beta 32');
});
test('homepage stat cards are polished and responsive', async ({ page }) => {
@@ -72,12 +71,12 @@ test('homepage browse summary links to grouped facet pages', async ({ page }) =>
const browse = page.getByRole('region', { name: 'Browse by catalog facets' });
await expect(browse.getByRole('heading', { name: 'Browse the archive' })).toBeVisible();
await expect(browse.getByRole('link', { name: /GBA\s+188/ })).toHaveAttribute('href', '/browse/platform/gba/');
await expect(browse.getByRole('link', { name: /Difficulty\s+105/ })).toHaveAttribute(
await expect(browse.getByRole('link', { name: /GBA\s+205/ })).toHaveAttribute('href', '/browse/platform/gba/');
await expect(browse.getByRole('link', { name: /Difficulty\s+127/ })).toHaveAttribute(
'href',
'/browse/type/difficulty/',
);
await expect(browse.getByRole('link', { name: /Complete\s+168/ })).toHaveAttribute(
await expect(browse.getByRole('link', { name: /Complete\s+348/ })).toHaveAttribute(
'href',
'/browse/status/complete/',
);
@@ -89,15 +88,15 @@ test('catalog search and filters update visible card count', async ({ page }) =>
await expect(page).toHaveTitle(/ROM Hack Archive/);
await expect(page.getByRole('heading', { name: 'ROM Hack Archive' })).toBeVisible();
const catalogCards = page.locator('[data-card-grid] [data-card]:visible');
await expect(catalogCards).toHaveCount(368);
await expect(catalogCards).toHaveCount(411);
await page.getByPlaceholder('Search hacks').fill('Radical Red');
await expect(catalogCards).toHaveCount(4);
await expect(catalogCards).toHaveCount(5);
await expect(page.getByRole('region', { name: 'Hack catalog' }).getByRole('link', { name: /Open Pokémon Radical Red/ })).toBeVisible();
await page.getByPlaceholder('Search hacks').fill('');
await page.getByLabel('GBA').check();
await expect(catalogCards).toHaveCount(188);
await expect(catalogCards).toHaveCount(205);
await page.getByLabel('Difficulty').check();
await expect(catalogCards.first()).toBeVisible();
@@ -181,10 +180,12 @@ test('detail page renders metadata and private file links', async ({ page }) =>
await expect(page.getByRole('heading', { name: 'Pokémon Radical Red' })).toBeVisible();
await expect(page.getByText('Platform').first()).toBeVisible();
await expect(page.getByText('Roster').first()).toBeVisible();
await expect(page.getByText('Fakemon').first()).toBeVisible();
await expect(page.getByRole('heading', { name: 'Files' })).toBeVisible();
await expect(page.getByRole('link', { name: 'ROM file' })).toHaveAttribute(
'href',
/https:\/\/romhacks-files\.ginnoir\.com\/_roms\/gba\/Hacks\//,
/https:\/\/romhacks-files\.ginnoir\.com\/_roms\/gba\/Pokemon%20-%20Radical%20Red/,
);
await expect(page.locator('body')).not.toContainText('undefined');
});
@@ -193,6 +194,6 @@ test('browse pages render static grouped lists', async ({ page }) => {
await page.goto('/browse/type/difficulty/');
await expect(page.getByRole('heading', { name: 'Difficulty' })).toBeVisible();
await expect(page.locator('[data-card]:visible')).toHaveCount(105);
await expect(page.locator('[data-card]:visible')).toHaveCount(127);
await expect(page.getByRole('link', { name: /Open Pokémon Radical Red/ })).toBeVisible();
});