Zum Inhalt

Tags across groups, made up as they're needed

Context and Problem Statement

Groups sort the inventory and the codex into one place each: a fire sword is under Weapons, the fire-resistance pot under Consumables, the fire grease somewhere else again. While playing, the player wants "everything fire" (before a burning boss, or to burn something), which the groups can't show. How do they get it, without keeping a list of tags by hand?

Goals and non-goals

  • Goal: tap a tag on a tab and see only what carries it there; with a group chip, both (fire and Weapons: the fire weapons).
  • Goal: no predefined tags; Claude makes one the first time it's needed and reuses it after, so "fire" doesn't split into "Feuer" and "fire damage". One flat tag per idea: "fire" covers dealing and resisting it.
  • Goal: tags fixed through the chat like everything else: rename, merge, take off a row.
  • Non-goal: a view across tabs ("fire" on items, codex and quests on one page). A tag filters the tab it's on.
  • Non-goal: tags on sessions, goals, notes, the character or stat snapshots.
  • Anti-goal: a tag giving away what the gate hides. A codex entry or quest the player hasn't reached stays hidden, its tags with it, and an unseen row's tag must not show up as a chip.

Considered Options

  • A list on each row: a tags JSON column on items, codex entries and quests, the set worked out from what's in use. Least schema, but a rename or merge rewrites every row, and nothing but Claude keeps near-duplicates out.
  • A tag table and one link table (chosen): tags holds each name once per game; tag_links joins a tag to an item, codex entry or quest by what and id, the pair correct already uses.
  • Groups that take several values: breaks the folding bands (one item under several headings) and mixes two ideas.

Decision Outcome

Chosen option: a tag table and one link table, because a rename or merge is one row and a duplicate name can't exist.

Data (migration 007_tags)

  • tags: id, game_id, name, created_at; unique on (game_id, name folded to lower case). Per game, like the inventory groups (games.item_kinds), so a second run of the same game starts with the same tags. Names in the player's words, short, lower case unless a proper name; no localName, since tags aren't the game's own names.
  • tag_links: tag_id, what (item | codex | quest), row_id; primary key on all three; a row's removal removes its links (in the same transaction as the row's own delete, since row_id points at three tables). A tag with no links left is deleted.
  • Every read joins through the row, so links are scoped by player and the spoiler gate (seen()) like the row itself.

Tools (MCP and the in-app chat)

  • record: items, codex entries and quests take tags: string[]. A name that matches an existing tag of the game (case-folded) reuses it; anything else creates one. The result lists newTags, and next asks Claude to say them back.
  • briefing, item, codex: return the tags in use in the playthrough with their ids and counts (tags: {id, name, count}[]), counted over what came through the gate, so Claude reuses them before filing. The tool descriptions and the MCP instructions say: reuse an existing tag; make a new one only for an idea none of them covers; one tag per idea, not per direction (fire, not fire damage and fire resistance).
  • item and codex take an optional tag to list only what carries it ("what do I have against fire?").
  • correct: what=item|codex|quest takes tags (the whole new list for that row). A new target what=tag renames one (name), merges it into another (mergeInto, its links move over) or removes it. Merging and removing only when the player asked, like merging quests.

Web app

  • Each item, codex entry and quest card shows its tags as small chips after its numbers.
  • On the items, codex and quests tabs, a tag row at the top: the tags in use on that tab with their counts, only when there are any. Tapping one shows only the cards that carry it, across the folding group bands; tapping it again clears it. One tag at a time. A band with nothing left is hidden, and a band's count shows what is left.
  • The tag row replaces the group chips of #37: the folding bands already give every group its place, and the player found the chip row redundant next to them.
  • The row is one paper strip with the tab's colour as its spine, like the group bands; its chips are shaped like luggage tags (a notch and a punched hole), since they hang on things across the groups. aria-pressed, keyboard focus on the whole button, the lucide Tag in a seal like the section headings, the part's Okabe-Ito tint.
  • Filtering happens in the browser (components/TagFilter.tsx), every card rendered on the server with data-tags; a link into a hidden card clears the tag (questlog:reveal).

REST

  • Items, codex entries and quests carry tags; the items and codex routes take ?tag=; GET …/playthroughs/{id}/tags lists the tags in use with their ids and counts, as the tools do.

Consequences

  • Good, because "everything fire" is one tap on whatever tab the player is on, and Claude keeps the set tidy as it files.
  • Good, because rename and merge are one change each and duplicates can't exist.
  • Bad, because tags are only as consistent as Claude's filing; the reuse rule and the list in every lookup are what keep them so.
  • Bad, because tag_links points into three tables without a foreign key; deletes must clean up links themselves (tested).