Skip to content
Shattered Crowns

v0.5.19-dev · 2026-07-27

Show Your Work

The follow-up list from v0.5.18, worked top to bottom. Two hardening items with real gameplay consequences, three that make the combat maths legible, and two that stop a decided fight costing a click per unit per round.

The engine finally checks range

It never did. Every gate in execute_ability interrogated the caster — alive, knows the ability, has the MP, isn't a toad — and not one looked at where the command was pointing. The only thing that had ever enforced reach was the client's red tile highlight, which is a drawing, not a rule.

Offline that cost a mis-click. Online it meant the authoritative server would resolve a spell aimed at the far corner of the board for any client that asked. RejectReason::OutOfRange { range, distance } carries both numbers, because "out of range" alone doesn't say how much closer to walk.

The gate reads combat::ability_reach, which is now also what the client paints and what the AI aims by — the AI had its own copy of the rule, and it had already fallen behind (it never learned about v0.5.13's rolled Jump range). Three answers to "how far can this go?" is a disagreement that costs the AI its turn.

An out-of-range cast is a refusal, not a whiff: nothing is spent.

Cheer has a budget

Brave multiplies every physical hit, and talk skills had no limit but the 5–100 clamp — so a Squire could spend the battle singing and arrive at Brave 100, an investment that keeps paying while only costing early turns. Joe's log has one casting Cheer four times in twenty-one turns.

MORALE_SHIFT_BUDGET caps talk at ±15 per stat per match — three casts. A budget rather than a lower ceiling, deliberately: a ceiling would silently do nothing for exactly the units that rolled well, and "why did my song not work on my best unit" is worse than a stated allowance everyone shares.

It is net, so an enemy Intimidate re-opens the headroom your Cheer spent. Two Orators arguing over one soldier is a fight worth having. The AI knows about the budget too, so it stops singing at someone who can't be moved.

Why is this number 27?

Hovering a target now shows the multipliers the hit actually collects:

Attack → Arcanist   21–27 DMG   Hit 84%
zodiac ×1.25  ·  brave ×0.65  ·  their DEF ×0.67

Green above 1.0, red below. Every zodiac roll in Joe's log fired silently — you could play a whole match without learning the wheel exists.

Each term is read from the same combat::*_multiplier function the resolver multiplies by, never re-derived in the UI. That rule is the whole point: the battle log spent eight releases printing "from BEHIND ×1.5" on magic hits precisely because the narration owned its own idea of the formula. Magic passes no facing at all, and its absence there is load-bearing.

The battle log gained the Brave term too (brave ×0.65), so "is my +5 Brave actually doing anything?" is a question the log can now answer by itself.

The last placeholder glyphs are gone

The charge hourglass, the death clock and the crystal were still Label3D text. v0.5.17 gave the 38 statuses real art and left these three behind with a note; this finishes the job through the same pipeline, with the death clock printing its count beside the skull.

smoke_overhead_markers (26 checks) counts ink rather than trusting that a file imported — ThorVG drops SVG features it can't parse without erroring, leaving a texture that loads and renders nothing. It also compares glyphs by an 8×8 shape signature rather than by total coverage: the hourglass and the skull land within 0.004 of each other on ink while looking nothing alike, so a coverage test would fail two good drawings and pass two identical ones.

Fast-forward (offline)

F, or the ▶▶ button. Speeds the bots up and auto-waits your own units on turns that have nothing in them — Joe's log runs ~15 consecutive turns of nothing but Wait while two death clocks tick down.

"Nothing in them" is ai::has_nothing_to_do, which runs the same decide the bots play by, so it can never skip a turn the game itself thought was worth taking. And the moment one of your units does have something to do, fast-forward switches itself off and says why. That auto-disengage is what makes an auto-wait safe to ship.

Offline only, on purpose: online there is a turn timer, teammates waiting, and a server that owns the turn.

Per-ability breakdown on the results board

The open item on ResultsBoard since v0.5.3. Each unit row now carries a third line beside its rolled kit and gear — what it actually landed:

✦ Attack ×7 · Dual Strike ×3 · Venom Dart ×2

Counts landed uses: the tally sits beside AbilityUsed, which the whiff rule already withholds when a cast reached nobody. Match History shows it too, since both doors render the same board.

Guard nets

ability_range (9), morale_budget (7), ability_usage (5), nothing_to_do (5), smoke_overhead_markers (26). Engine 41 suites and server 21, all green.

Three existing tests were casting out of range and had to be repositioned — a monk aiming eight tiles away, a mage five, a knight six. Each was fine while nothing checked geometry, and each is now the refusal it always described. One documents a real limitation rather than asserting past it: decide bails to Wait as soon as the visible-enemy list is empty, so with every enemy down or Invisible the fast-forward reads a healer's turn as empty even with a dying ally in reach. Bounded and acceptable, and now written down.