Repository navigation
Conversation
A layout can now declare a section, a region whose rules text the engine
splits into rows, one for each loyalty ability, saga chapter, or class
level, spread top to bottom and shrunk together until every row fits.
A row can carry a badge or number beside it, such as a loyalty cost or
a chapter's numeral, and template.PlacedRow.FirstLineCenter anchors
that badge on the row's own opening line instead of the center of
however many lines it wraps to. Manifest contract raised to 1.2, which
only adds optional fields.
Supporting additions: template.TextBoxSpec.Prefix (and
manifest.TextStyle.Prefix) draws literal text before a box's own
value, such as the classic frame's "Illus. " artist credit in place of
the modern frame's nib icon. An art window's "after" is now optional,
so a layout can place its art beneath the whole stack. frame.Condition
gains loyalty, chaos, snow and nonsnow features and a count comparison
such as "rows>3" for a condition that depends on how many rows a
section has. card.flipsideType now reads the other face's own single
subtype ("Tibalt", "God") instead of always its supertype
("Planeswalker", "Creature").
gofmt -l ., go vet ./... and go test -race ./... all clean in engine/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRUBuanCoioaJx1YjULfbQ
…date the bridge golden engine/frame.Condition now accepts a count comparison such as "rows>3" alongside its plain feature vocabulary (odevine#134). The studio's own vocabulary test compiled every condition name on its own and failed on the new count names, which only compile with a comparison after them; it now tries that form too before failing. card.Data and the types built from it gained a Defense field for a battle card's defense stat, which crosses the bridge, so bridge.golden needed regenerating (go test ./internal/desktop -run Bridge -update). No functional change: the app already renders a battle's defense and accepts count conditions correctly, since both come from the engine. This just keeps the studio's own tests and the bridge golden in step with it. Depends on odevine#134 landing in a released engine; go.work (not committed) stands in for that pin locally. go test ./... passes in ui/ with that in place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRUBuanCoioaJx1YjULfbQ
| }, | ||
| reflect.TypeOf(manifest.RowItem{}): { | ||
| "for": {"enum": append([]string{""}, card.RowFields()...), "description": "The row field the item shows. Empty draws for every row"}, | ||
| "anchor": {"enum": []string{"", "center", "top"}}, |
There was a problem hiding this comment.
The anchor enum leaves out first-line, which the validator (manifest/sections.go) and placeSection both accept and which this PR introduces. A schema-driven editor or lint would reject the new anchor. Add it to the enum and cover it with a test in author.
| nodes = append(nodes, got...) | ||
| } | ||
| } | ||
| for _, ps := range placed { |
There was a problem hiding this comment.
Section text and images skip the controls that regular boxes honor. Boxes check req.Hides("text", key), call req.Report and check ctx.Err() per job (see the loop just above), and none of that happens here or for sectionImages. ui drives Hide (pipeline, studio), so a hide toggle cannot affect a section's rows. A new hide kind such as "section" (or reusing text with the abilities.rows[1] keys that Describe already reports) would close the gap.
|
|
||
| // placeSection splits the face's rules text into the section's rows, lays them | ||
| // out, and lists what each row draws | ||
| func placeSection(req template.RenderRequest, t *template.Template, layout *template.Layout, authored image.Point, job sectionJob, fc *faces, taken card.Taken, syms symbols, fr *fonts.Resolver, scale template.Scale) (*placedSection, error) { |
There was a problem hiding this comment.
Row text is drawn through sectionPiece.textNode and RenderTextBox directly instead of placeBox/drawBox, so box features (ClearOf, Space, Rotate, Flow) do not apply inside a section, and manifest.Section.Text accepts a TextStyle that may imply otherwise. If that is intended, say so in the Section doc comment. Otherwise consider validating that unsupported fields are unset, or routing rows through the box pipeline.
| } | ||
| // The text keeps out of the rectangle where each row landed without it, so a | ||
| // row beside it wraps narrower and the rows settle again | ||
| tops := make([]int, len(out.Rows)) |
There was a problem hiding this comment.
The avoid pass is a single iteration. Rows are laid out once without the avoid rectangle, then re-measured with each row pinned to its first-pass top (tops). When avoidance rewraps a row and changes its height, later rows move, but they are measured against the stale tops, so a row can end up beside or outside the area it was measured for. It converges for the fixtures, but a short loop to a fixed point (or a comment on the limitation) would make it dependable. A test with an avoid rectangle that changes a row's height would show it.
| type Row struct { | ||
| // Cost is what the row is activated for: a loyalty ability's cost such as | ||
| // "+1" or "−3", or the mana a class level costs | ||
| Cost string |
There was a problem hiding this comment.
Cost holds both a loyalty ability's cost and a class level's mana cost, and keyField (render/section.go) has to special-case which field marks a row. It works, but a separate field or an explicit accessor per split would keep the two meanings from leaking into templates (for: "cost" means different things for loyalty and levels).
| // supertype or type otherwise ("Creature", "Sorcery"). A card can print more | ||
| // than one subtype, and the rule about which single word to show then does | ||
| // not generalize cleanly, so this keeps to the common case rather than guess | ||
| func flipsideType(d *Data) string { |
There was a problem hiding this comment.
flipsideType now prefers the other face's single subtype over its type, which changes output for existing MDFC templates (for example a Creature face now prints its one subtype rather than "Creature"). The behavior change is in the description but deserves its own test against an existing template's expectations, and possibly a separate box name such as flipside_subtype so current layouts do not shift.
| // MaxDimension is the largest width, height, or position in a canvas | ||
| MaxDimension = 20000 | ||
| MaxParts = 256 | ||
| MaxParts = 1024 |
There was a problem hiding this comment.
MaxParts goes from 256 to 1024 without a note. Say what template needs it. This is also part of a wider scope: the PR adds a leveler parser, prototype/mutate/chaos boxes, defense, SlotPrototype, Keys.Snow/Prototype and the boxFields→textStyle refactor, none of which the title or summary mention. Splitting these or listing them in the description would make this easier to review.
Summary
A layout can now declare a section, a region whose rules text the engine splits into rows, one for each loyalty ability, saga chapter, or class level, spread top to bottom and shrunk together until every row fits. A row can carry a badge or number beside it, such as a loyalty cost or a chapter's numeral, and
template.PlacedRow.FirstLineCenteranchors that badge on the row's own opening line instead of the center of however many lines it wraps to.Supporting additions, all needed by templates this makes possible:
template.TextBoxSpec.Prefix(andmanifest.TextStyle.Prefix) draws literal text before a box's own value, such as the classic frame's "Illus. " artist credit in place of the modern frame's nib icon.afteris now optional, so a layout can place its art beneath the whole stack.frame.Conditiongainsloyalty,chaos,snowandnonsnowfeatures and a count comparison such asrows>3for a condition that depends on how many rows a section has.card.flipsideTypenow reads the other face's own single subtype ("Tibalt", "God") instead of always its supertype ("Planeswalker", "Creature").Manifest contract raised to 1.2, which the doc comment already promises only adds optional fields, so this is not a breaking change.
This is the first of a planned sequence of PRs that together bring in a batch of Proxyshop-converted template families (M15, Classic, Planeswalker Borderless). This PR has no dependency on the others; the next PR (the psdconvert/tplmerge conversion tooling) does depend on the manifest/validate changes here.
Test plan
gofmt -l .clean inengine/go vet ./...clean inengine/go test -race ./...passing inengine/origin/main, with none of the other in-flight work mixed in🤖 Generated with Claude Code
https://claude.ai/code/session_01XRUBuanCoioaJx1YjULfbQ