Fix PF2e actions: dedupe junk, show action cost, add cost filter

The AoN action index was polluted with ~2,300 trait-fragment entries
('(concentrate)', 'envision', 'command'...) and the same basic actions repeated
per source. Filter to real Title-cased names and dedupe by name (keep richest):
3,950 -> 646 real actions (also cleaned in the live data file, 3.9M -> 859K).

- pf2eMeta now shows action cost (Single Action/Reaction/...) instead of 'common'
- Added an Action cost filter to the Actions category
- Hide noisy 'common' rarity in PF2e detail facts
- Scraper postProcess() makes future fetches clean too

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 01:10:00 +02:00
parent 2c08a26f21
commit 1c87b0e3aa
4 changed files with 45 additions and 6 deletions
File diff suppressed because one or more lines are too long
+25 -1
View File
@@ -95,12 +95,36 @@ function normalize(src: Record<string, unknown>): Record<string, unknown> {
return out; return out;
} }
/**
* The AoN `action` index is polluted with action-trait fragments (names like
* "(concentrate)", "envision", "command") and the same basic actions repeated
* once per source. Keep only real Title-Cased action names and dedupe by name,
* preferring the entry with the most complete text.
*/
function isRealAction(x: Record<string, unknown>): boolean {
const n = String(x.name ?? '').trim();
return n !== '' && /^[A-Z0-9"]/.test(n) && !n.startsWith('(');
}
function dedupeByName(list: Record<string, unknown>[]): Record<string, unknown>[] {
const best = new Map<string, Record<string, unknown>>();
for (const x of list) {
const k = String(x.name);
const cur = best.get(k);
if (!cur || String(x.text ?? '').length > String(cur.text ?? '').length) best.set(k, x);
}
return [...best.values()].sort((a, b) => String(a.name).localeCompare(String(b.name)));
}
function postProcess(category: string, list: Record<string, unknown>[]): Record<string, unknown>[] {
if (category === 'action') return dedupeByName(list.filter(isRealAction));
return list;
}
async function main() { async function main() {
await mkdir(OUT_DIR, { recursive: true }); await mkdir(OUT_DIR, { recursive: true });
const index: { category: string; file: string; count: number }[] = []; const index: { category: string; file: string; count: number }[] = [];
for (const [category, file] of Object.entries(CATEGORIES)) { for (const [category, file] of Object.entries(CATEGORIES)) {
const entries = await fetchCategory(category); const entries = postProcess(category, await fetchCategory(category));
const path = join(OUT_DIR, `${file}.json`); const path = join(OUT_DIR, `${file}.json`);
await writeFile(path, JSON.stringify(entries)); await writeFile(path, JSON.stringify(entries));
index.push({ category, file, count: entries.length }); index.push({ category, file, count: entries.length });
+3 -1
View File
@@ -132,7 +132,9 @@ export function Pf2eDetail({ entry }: { entry: CompendiumEntry }) {
['Damage', entry.damage], ['Damage', entry.damage],
['Source', entry.source], ['Source', entry.source],
]; ];
const shown = facts.filter(([, v]) => v !== undefined && v !== null && v !== ''); const shown = facts.filter(
([k, v]) => v !== undefined && v !== null && v !== '' && !(k === 'Rarity' && v === 'common'),
);
const text = (entry.text as string | undefined) ?? (entry.summary as string | undefined) ?? ''; const text = (entry.text as string | undefined) ?? (entry.summary as string | undefined) ?? '';
return ( return (
+16 -3
View File
@@ -110,10 +110,20 @@ const levelMax = (): FilterDef => ({
function pf2eMeta(e: Entry): string { function pf2eMeta(e: Entry): string {
if (typeof e.level === 'number') return `Lvl ${e.level}`; if (typeof e.level === 'number') return `Lvl ${e.level}`;
if (e.rarity) return String(e.rarity); // action cost is the meaningful label for actions/activities
return ''; if (e.actions) return String(e.actions);
const rarity = e.rarity ? String(e.rarity) : '';
return rarity && rarity !== 'common' ? rarity : '';
} }
const actionCostFilter = (): FilterDef => ({
key: 'actions',
label: 'Action cost',
kind: 'eq',
get: (e) => e.actions as string | undefined,
options: (d) => distinctStrings(d, (e) => e.actions),
});
function pf2eCategory(id: string, label: string, file: string, opts: { level?: boolean; linkAs?: 'spell' | 'item' } = {}): CategoryDef { function pf2eCategory(id: string, label: string, file: string, opts: { level?: boolean; linkAs?: 'spell' | 'item' } = {}): CategoryDef {
const filters: FilterDef[] = [traitFilter(), rarityFilter()]; const filters: FilterDef[] = [traitFilter(), rarityFilter()];
if (opts.level !== false) filters.unshift(levelMin(), levelMax()); if (opts.level !== false) filters.unshift(levelMin(), levelMax());
@@ -229,7 +239,10 @@ export const CATEGORIES: CategoryDef[] = [
pf2eCategory('heritages', 'Heritages', 'heritages', { level: false }), pf2eCategory('heritages', 'Heritages', 'heritages', { level: false }),
pf2eCategory('backgrounds', 'Backgrounds', 'backgrounds', { level: false }), pf2eCategory('backgrounds', 'Backgrounds', 'backgrounds', { level: false }),
pf2eCategory('archetypes', 'Archetypes', 'archetypes', { level: false }), pf2eCategory('archetypes', 'Archetypes', 'archetypes', { level: false }),
pf2eCategory('actions', 'Actions', 'actions', { level: false }), {
...pf2eCategory('actions', 'Actions', 'actions', { level: false }),
filters: [actionCostFilter(), traitFilter()],
},
pf2eCategory('conditions', 'Conditions', 'conditions', { level: false }), pf2eCategory('conditions', 'Conditions', 'conditions', { level: false }),
pf2eCategory('deities', 'Deities', 'deities', { level: false }), pf2eCategory('deities', 'Deities', 'deities', { level: false }),
]; ];