Add lore texts
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Guidance for agents that work in this repository. [README.md](README.md)
|
||||
describes what the app does for its users. This file describes how it is built.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
npm test # the parity suite, see "Tests"
|
||||
npm run build # static files in dist/
|
||||
npm run fixtures # regenerate test fixtures, see "Tests"
|
||||
```
|
||||
|
||||
`npm run lint` reports findings that come from upstream, mostly
|
||||
`a11y/useButtonType`. These findings are old, so lint is not part of CI. Before
|
||||
you treat a finding as new, compare it against `git show upstream/main:<file>`.
|
||||
|
||||
## Where the transforms fit
|
||||
|
||||
```
|
||||
upload/.rosz -> unzip -> DOMParser -> applyTransforms -> Create40kRoster* -> render
|
||||
^
|
||||
src/transforms/index.js
|
||||
```
|
||||
|
||||
`src/App.jsx` runs the transforms between the parse of the XML and the build of
|
||||
the roster. A toggle therefore rebuilds the cards from the original XML, and
|
||||
the user uploads nothing a second time. For the same reason, a saved roster
|
||||
holds the raw roster XML and not the parsed object.
|
||||
|
||||
[`src/transforms/config.js`](src/transforms/config.js) lists the abilities that
|
||||
the transforms strip and split. When you find more of them, add them to
|
||||
`abilitiesToStrip` and `abilitiesToConvert`.
|
||||
|
||||
## Lore text
|
||||
|
||||
[`src/helpers/lore.js`](src/helpers/lore.js) matches a roster name against
|
||||
`public/Lore.csv` in three steps:
|
||||
|
||||
1. The normalized name. Case, accents, curly quotes and punctuation all differ
|
||||
between the roster and the export.
|
||||
2. The name with its last word in the singular form. Example: "Myphitic
|
||||
Blight-haulers" against "Myphitic Blight-hauler".
|
||||
3. The longest known name that the roster name *ends* with. Example: "Thousand
|
||||
Sons Chaos Spawn" against "Chaos Spawn".
|
||||
|
||||
This search resolves every unit in the bundled 10th-edition examples.
|
||||
|
||||
`parseLore` finds columns by header name. If someone exports the file again
|
||||
with a `faction` column, `parseLore` can match that column against the
|
||||
catalogue of the force. Nothing else has to change. Today the export has no
|
||||
such column, so the longest entry wins for a name with lore in more than one
|
||||
faction.
|
||||
|
||||
Two properties of the card header are less obvious than they look:
|
||||
|
||||
- The italic text needs a **fourth font file**. `ConduitITCStd` shipped as
|
||||
three upright faces, and `:root` in [`src/index.css`](src/index.css) sets
|
||||
`font-synthesis: none`. A request for italic therefore printed upright text
|
||||
and gave no warning. `public/fonts/ConduitITCStd Italic.woff2` and its
|
||||
`@font-face` rule correct this. The element also sets
|
||||
`font-synthesis: style`. As a result, a face that fails to load degrades to a
|
||||
slanted upright face and not to no italic at all.
|
||||
- The lore panel has absolute position, so it cannot make the header taller.
|
||||
Without help, a long legend is clipped: the longest entry in the export
|
||||
overruns a 15rem header at each width below approximately 1300px. A
|
||||
`ResizeObserver` on the text feeds the `min-height` of the header instead.
|
||||
The observer is necessary because the text wraps differently at each card
|
||||
width.
|
||||
|
||||
Only the 10th-edition and 11th-edition renderers show lore. Leave the
|
||||
9th-edition renderer in `src/9th/` alone. It lays out its header differently.
|
||||
|
||||
## Tests
|
||||
|
||||
The transforms began as three standalone Python scripts
|
||||
(`merge_duplicate_units`, `remove_leader_abilities`, `convert_choice_abilities`)
|
||||
that rewrote the `.ros` file before the upload to FancyScribe. The scripts are
|
||||
gone, but the tests measure against their output.
|
||||
|
||||
`src/transforms/__fixtures__/` holds four 11th-edition rosters. For each roster
|
||||
it also holds the output of those scripts: `<name>.{merge,strip,convert,all}.ros`.
|
||||
The suite runs each transform over the input and asserts that the result is the
|
||||
same roster. This is worth more than a snapshot of the current code, because
|
||||
the expected output comes from a **different implementation**. A person
|
||||
verified that implementation with printed cards.
|
||||
|
||||
CAUTION: Do not regenerate an existing fixture from the JavaScript code. The
|
||||
test then compares the code against itself and passes whatever the code does.
|
||||
`scripts/update-fixtures.mjs` therefore refuses to overwrite a fixture without
|
||||
`--force`. Use the script when you add a new example roster, and read the diff:
|
||||
|
||||
```sh
|
||||
# Put a new roster in src/transforms/__fixtures__/, then:
|
||||
npm run fixtures
|
||||
```
|
||||
|
||||
The comparison is structural, not textual. The Python scripts edited the file
|
||||
as text and kept its format byte for byte. These transforms build DOM nodes, so
|
||||
attribute order and whitespace differ legitimately. The suite compares
|
||||
generated `id` and `typeId` values as *tokens*. The requirement is that ids are
|
||||
shared and distinct in the same pattern, not that both implementations hash
|
||||
alike.
|
||||
|
||||
`integration.test.js` pushes the transformed document through the real roster
|
||||
parser and checks what lands on the card:
|
||||
|
||||
- one Skitarii card that carries both weapons
|
||||
- the data-tether ability on the model that brings it
|
||||
- no Support ability
|
||||
- one row for each Canticle
|
||||
|
||||
The tests run under jsdom, which does not implement scoped selectors like a
|
||||
browser. `force.querySelectorAll("force>selections>…")` finds nothing under
|
||||
jsdom. A browser matches the selector against the whole tree and then keeps the
|
||||
descendants of `force`, so the parser found no units at all. The two call sites
|
||||
that depend on this behavior now use `:scope>…`, which works in both.
|
||||
|
||||
## Merges from upstream
|
||||
|
||||
FancyScribe is under active development, and 11th-edition support landed
|
||||
recently. This fork touches little of it. The `upstream` remote is configured:
|
||||
|
||||
```sh
|
||||
git fetch upstream
|
||||
git merge upstream/main
|
||||
```
|
||||
|
||||
Expect conflicts only in `src/App.jsx`, `index.html`, `vite.config.js` and
|
||||
`package.json`. `src/transforms/` is completely new and never conflicts.
|
||||
|
||||
The fork also hides three things that upstream shows, because a generic
|
||||
datasheet cannot use them: the roster overview card with its charts, the unit
|
||||
composition, and the points cost of each unit. These decisions live in
|
||||
[`src/fork.js`](src/fork.js), which upstream does not have.
|
||||
`src/10th/Roster.jsx` uses them on as few lines as possible:
|
||||
|
||||
- It imports `ShortSummaryTable` from `../fork`, not from `./ShortSummaryTable`.
|
||||
This one-line change leaves the render site untouched. The upstream component
|
||||
stays in the tree, unused, so its future diffs continue to apply.
|
||||
- `hideModelCount` is pinned to `HIDE_UNIT_COMPOSITION` and is no longer a
|
||||
piece of checkbox state.
|
||||
|
||||
Only the two checkboxes and the `pts` span are deleted. A merge that touches
|
||||
them therefore reports a conflict instead of quietly bringing them back.
|
||||
|
||||
`vite.config.js` sets `base: "/"`, because the app is served at the root of a
|
||||
domain. Upstream sets `/fancyscribe` for GitHub Pages. If you serve the app
|
||||
from a subpath, change this setting.
|
||||
Reference in New Issue
Block a user