Skip to content

Make campaign heroes readable next to their own troops - #152

Merged
Knorcedger merged 8 commits into
mainfrom
claude/wallace-outfit-differentiation-nv9yvs
Aug 23, 2026
Merged

Make campaign heroes readable next to their own troops#152
Knorcedger merged 8 commits into
mainfrom
claude/wallace-outfit-differentiation-nv9yvs

Conversation

@Knorcedger

@Knorcedger Knorcedger commented Aug 23, 2026

Copy link
Copy Markdown
Owner

What changed?

Campaign heroes were indistinguishable from the soldiers they lead. Worse than "similar": heroes alias a rank-and-file rig through UnitDef.sprite (heroWallacechampion), and in the HD art pack that loads by default unit/champion/* and unit/militia/* are the same frames — so William Wallace was the militia sprite, in chapters that are lost the moment he dies.

Heroes are now marked in @bf/data with a hero flag plus a master-palette accent ramp (heroCloth), and the renderer keys a hero treatment off that flag, so every existing and future hero in every campaign gets it:

  • Colour — the rig's cloth and metal ramps are palette-swapped to the hero ramp. Both families are needed: the champion, knight, paladin and cavalier rigs contain zero cloth pixels (harness covers the tunic), while militia and man-at-arms rigs are almost all cloth.
  • Tint — the sprite is multiplied by the ramp's light tone, lifted to a fixed peak and deepened by a saturation exponent. This is what carries the accent on the pre-rendered HD art, where no palette colour survives an exact-match swap.
  • Size — hero art draws at 1.3×, and hero health bars anchor to the sprite's visible top instead of the fixed unit offset, which the larger art crossed at chest height.
  • Marker — an eight-pointed star squashed onto the floor plane at the feet, plus a five-pointed star above the health bar so the marker survives a crowd that hides the ground. Both in the hero's own colour over a dark rim with a highlight core. The minimap adds a matching gold pip.

Player colours are untouched throughout, so a hero still shows whose side he is on, and heroes keep their accent through the corpse animation instead of popping back to rank-and-file art.

Related issue

Refs #110

Partial delivery on purpose: this is the render-time treatment. Bespoke hero rigs in tools/assetgen — real distinct silhouettes — remain open on the issue, as do hero HUD portraits (a hero still shows his base unit's icon).

Why this approach?

  • Why not bespoke hero art now? It is the proper fix and the code comments have been anticipating it, but it means ~100 new frames per hero across 15 heroes, re-checking the 2048 px atlas budget, and matching art in the HD pack or heroes look different between art modes. The render-time treatment lands the readability fix without that.
  • Why both a palette swap and a tint? They cover different art. The exact-match swap gives a clean result on the master-palette atlas and any future baked art; it does nothing on the HD pack (anti-aliased 3D renders, ~1500 distinct colours per frame, zero palette pixels). The tint works on anything. Both are driven from the same hero ramp, so a hero is the same colour in either mode.
  • Ramp constraints. Hero ramps must be master-palette colours, must differ from the cloth and metal ramps they replace (equal to either = a no-op), must be saturated enough to survive as a multiply tint (a grey or near-white ramp multiplies to nothing), and must differ from the other heroes in the same scenario's cast. All four are enforced by tests.
  • Why not the existing gold ring? #E6C04A already marks worksite highlights, rally flags, garrison badges, conversion beams and impact flashes, so a gold ellipse read as generic UI. Heroes get their own shape and their own per-hero colour.
  • Determinism: none of this touches packages/sim — it is render-only, keyed off render-only data fields alongside the existing sprite and icon.

Merge with main

main moved while this was open (#116/#134 centralised art scale and the frame resolver, #142 renamed the gather highlight to the worksite highlight, and the unit art was regenerated). Resolved by folding the hero work into the shared helpers rather than keeping a parallel path: the hero draw scale is now a case inside artScaleForFrame, so the live sprite, fog ghost and placement preview keep sharing one rule, and the accent flows through resolveCandidates/buildingArtKey. The standalone artScaleFor this branch had added is gone.

Verification

  • npm run typecheck
  • npm test — 1410 passing, 11 skipped (post-merge; the branch's own suite was 1047 before main was merged in)
  • npm run build and npm run bundle:check
  • I added or updated tests where behavior changed
  • I included screenshots or a recording for visible changes

Screenshots were taken and reviewed during development but could not be attached from this environment. Visual verification: Wallace chapter 1 driven in a headless browser (wallace-1 via the nav hint) at 1280×800 @2×, in the default HD art mode and in the pixel-atlas mode, unselected and selected, checking the accent, both stars, the health-bar stack and the minimap pip — and re-run after the merge with main, since main regenerated the unit art. No console errors. Ownership was checked per cast: each hero tint over its player's colour band (red stays red, green stays green, yellow stays warm).

New tests:

  • packages/data/src/data.test.ts — hero flag coverage; ramps are palette-legal (validated against tools/assetgen's master palette), saturated, not equal to the cloth or metal ramps, and distinct within each scenario cast.
  • packages/game/src/heroArt.test.ts — measures the shipped atlas: every hero rig gets 8–17% of its opaque pixels repainted. This is the check that catches an accent aimed at a ramp its rig does not use.
  • packages/game/src/recolor.test.ts — the combined single-pass palette application, including that the accent cannot consume pixels the player swap just wrote.
  • packages/game/src/frames.test.ts, world.test.ts — hero helpers, tint derivation and bounds, star geometry, hero health-bar offset.

Provenance and AI assistance

Written by Claude Code (Anthropic) working from the issue and in-session review by the maintainer: it wrote the data, renderer and test changes, measured the atlases to pick the accent targets, and drove the headless browser for visual checks. No generated art assets — no atlas, sprite or texture file is added or modified by this branch; the accent is computed at runtime from existing frames. All commits signed off under the DCO.

Contributor checklist

  • I searched issues and active pull requests before implementation and coordinated ownership in the linked issue
  • This pull request closes or references exactly one primary coordination issue
  • I read CONTRIBUTING.md and followed the deterministic simulation rules
  • I reviewed and understand every submitted change
  • I have the right to submit this code and any included assets
  • My commits are signed off under the Developer Certificate of Origin (git commit -s)
  • This PR contains no credentials, private player data, or unlicensed third-party material

Heroes alias a rank-and-file rig through UnitDef.sprite, and in the HD art
pack a hero and his militia are literally the same frames, so William Wallace
was indistinguishable from the soldiers he leads — in chapters that are lost
the moment he dies.

Mark heroes in @bf/data with `hero` plus a master-palette accent ramp, and give
the renderer a hero treatment driven off that flag, so every existing and future
hero in every campaign gets it:

- palette-swap the rig's cloth AND metal ramps to the hero ramp (which of the
  two a rig uses depends on its tier: militia are cloth, champions and knights
  are all harness, so aiming at one family only would miss half the roster);
- multiply the sprite by the ramp's light tone lifted to a fixed peak, which is
  what carries the accent on the pre-rendered HD art where no palette colour
  survives an exact-match swap;
- draw hero art at 1.15x, keep a permanent gilded ring at the feet, and add a
  gold pip on the minimap.

The player-colour band keeps its own ramp throughout, so a hero still shows
whose side he is on, and dying heroes keep their accent through the corpse
animation instead of popping back to rank-and-file art.

Tests cover the data flag and ramp constraints (palette-legal, saturated enough
to tint, distinct within each scenario's cast), the single-pass recolour, the
hero helpers, and — against the shipped atlas — that each hero rig really does
get a visible share of its pixels repainted.

Refs #110

Signed-off-by: Claude <knorcedger@gmail.com>
Follow-up on the hero treatment after seeing it in play:

- The gold ring read as "yet another amber ellipse": #E6C04A already marks gather
  targets, rally flags, garrison badges, conversion beams and impact flashes.
  Heroes now get their own shape instead — a four-pointed compass star squashed
  onto the floor plane at the feet, plus a five-pointed star floating above the
  health bar so the marker survives a crowd that hides the ground. Both are drawn
  in the hero's own accent colour over a dark rim with a pale core, so a dark or
  green hero still separates from grass.
- Tint deepened: the ramp's light tone keeps its lifted peak, but the two
  non-dominant channels are pushed down by a saturation exponent, so the hue is
  much stronger without dimming the sprite into a smudge.
- Hero draw scale 1.15x -> 1.3x.
- Hero health bars anchor to the sprite's visible top instead of the fixed unit
  offset, which the larger art crossed at chest height.

Refs #110

Signed-off-by: Claude <knorcedger@gmail.com>
Four points read as too plain a marker. starPoly gains a minorRatio so every
second point can be shortened, and the ground marker uses it for four long
cardinals plus four short diagonals: uniform eight points at marker size just
fill in as a circle, which is the shape the hero marker exists to avoid.

Refs #110

Signed-off-by: Claude <knorcedger@gmail.com>
Eight points of one length, as asked. A deeper inner radius (0.28) keeps them
spiky at marker size instead of closing into a disc, so the alternating-length
compass rose is no longer needed: starPoly loses the minorRatio parameter it
added and goes back to a plain star generator.

Refs #110

Signed-off-by: Claude <knorcedger@gmail.com>
@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bannerfall Ready Ready Preview Aug 23, 2026 10:21am

Request Review

…-differentiation-nv9yvs

Signed-off-by: Claude <knorcedger@gmail.com>

# Conflicts:
#	CHANGELOG.md
#	packages/game/src/frames.test.ts
#	packages/game/src/world.test.ts
#	packages/game/src/world.ts
…-differentiation-nv9yvs

Signed-off-by: Claude <knorcedger@gmail.com>
…-differentiation-nv9yvs

Signed-off-by: Claude <knorcedger@gmail.com>
…-differentiation-nv9yvs

Signed-off-by: Claude <knorcedger@gmail.com>
@Knorcedger
Knorcedger merged commit 7610fc7 into main Aug 23, 2026
3 checks passed
@Knorcedger
Knorcedger deleted the claude/wallace-outfit-differentiation-nv9yvs branch August 23, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant