Skip to content
Shattered Crowns

v0.5.9-dev · 2026-07-19

Three Turns to Rise

Shipped as a single commit, 9c04436, whose message describes only the death-clock fix — a pre-push hook published it before the message could be widened. This section is the authoritative record of what that commit contains; the release pipeline reads from here, not from git log.

A playtest-notes release. Every item traces to Joe's second real session or to a question it raised — starting with the bug that had been quietly deleting units before anyone could save them.

Fixed — the death clock actually ticks (ct_system.rs)

tick_death_counters ran on every tick_ct call, and the CT loop spins many times per turn. A KO'd unit went 3 → 2 → 1 → crystal in about a third of a second of wall clock — visible in Joe's log at t=143.40 → 143.73. Raise, Phoenix Down and the whole crystal-absorb mechanic were effectively unreachable in a real fight.

The counter is now paced by the body's own CT: KO'd units keep accruing CT and keep their place in the turn order, and the clock ticks once when their number comes up. Three of the dead unit's turns, which is what FFT means and what the number 3 always implied. Pinned by three tests, including the exact tick count.

Fixed — the Movement slot was inert again (equip_roll.rs)

Move +2, Jump +3, Teleport and Levitate did nothing in a real match. Again.

The slot was woken in v0.5.8, and apply_loadout_to_unit layers it correctly: equipment rebuild, then supports, then movement, because recalc_stats REBUILDS mov/jump/teleport/levitate from job base plus gear and wipes anything already sitting on top. That part was right and stayed right.

But every real match — server MatchInstance::new, offline sc_engine — calls roll_battle_equipment AFTER unit creation, and that re-runs recalc_stats_from_db to fold in the rolled gear. It replayed the support layer afterwards and not the movement one, so a thief's Move +2 was rebuilt away before the first tick. Measured: 6 MOV equipped, 6 MOV unequipped.

The roll now replays all three layers in the same order. What makes this worth writing down is why 22 movement tests stayed green through it: they all build units with create_match_for_loadouts, which stops short of the gear roll. The tests proved the layering was correct and it was — they just never ran the path that follows it. The new guard movement_bonus_survives_the_equipment_roll calls the roll in the same position MatchInstance::new does, which is the only position that matters.

Verified by revert: the test fails 6-vs-8 without the fix.

Added — Dragoon Jump leaves the board (state.rs, abilities.rs)

Jump was a plain charged physical hit: the dragoon stood still for the whole charge, perfectly killable, which made a 5-CT Dragon Dive strictly worse than a free basic attack. Now a Jump sets Unit.airborne, and while it is set the unit is untargetable and undamageable by everything (one gate, at the single target lookup every ability funnels through), out of the turn order, and invisible to the AI. On resolve it lands on the aimed tile — or the nearest free neighbour, because two units never share a square — and swings.

New events UnitTookFlight / UnitLanded narrate it. Five tests cover both ends of the window.

Added — playtest mode (server/src/playtest.rs)

SC_PLAYTEST_UNLOCK_ALL=1 on the server makes every account report every job unlocked with every ability learned, across all four R/S/M slots. It is a server flag, not a per-account one, on purpose: balance feedback is only comparable if the whole test is playing by the same rules.

The unlock is computed at read time and never written to disk, so turning the flag off restores real progression exactly. Both halves are wired — what the profile reports AND what hydrate_loadout accepts at READY — because a client showing an unlocked picker while the server rejects the pick is exactly the v0.5.5 "job 'knight' is locked" bug.

Changed — the reveal ceremony

  • Gear now rolls face-down alongside the skills instead of arriving face-up. Same queue, same timer, and an ultimate item pops the same way an ultimate skill does. Gear has its own rarity ladder and watching it turn over is the same thrill.
  • Study countdown 10s → 30s. There is a full loadout to read per unit now, and at 3v3 that is six cards. A click still starts immediately.

Changed — balance pass

  • JP Boost +50% → +25%. At half a job level per match it was the default Support pick for anyone still levelling, which is one slot the other 30 supports never got to compete for.
  • Squire gains Ultima (ultimate) — non-elemental, AoE 2. FFT teaches it to whoever survives being hit by it; the lowest job in the tree ending up with the highest spell is the joke, and it is worth keeping.
  • Knight gains Stasis Sword (ultimate) — a Holy line attack that Stops everything it passes through.
  • Earth Slash is a line, not a single target. It was authored range-4 aoe-0, which made it a strictly worse Pummel.
  • X Break destroys gear. Armor Break and Weapon Break now remove the target's body armour / weapon for the rest of the battle, on top of the -30% status. Gear is rolled per battle and never owned, so this can only ever cost something temporary. The subtraction is a pure stat delta, NOT a recalc_stats — a rebuild would erase every active status and support mod.
  • Induration is faith-gated: CFa/100 × TFa/100 × (MA + 120). Both faiths matter, which makes low Faith real defensive value rather than only weaker magic. Misses emit StatusResisted with the odds.
  • Nameless Dance scatters a random affliction, rolled per target.
  • Promoted to ultimate: Masamune, Silent Dirge, Stop, Induration, Nameless Dance. Silent Dirge's Sleep is pinned at 2 of the target's turns.
  • Erebus retired — the summoner carried two identical 999-JP ultimates.
  • Ultimate cap raised 12 → 20, deliberately: the target shape is now roughly one chase prize per job. A new guard caps any single job at two.

Fixed — the roll ceremony plays on EVERY match

The reveal was gated on "this match id differs from the last one I saw", where the memory (NetworkManager._logged_match_id) was an autoload field that outlived the match. Offline match ids ARE the map seed, so a rematch on a pinned battlefield/seed collided outright and showed no ceremony at all; the server's u64 ids also lose their bottom 11 bits crossing JSON into a GDScript float.

Starting a match is an explicit event, so it now says so explicitly: the two real entry points (start_offline_match, and the first MatchState after an online auth) arm a flag that the snapshot consumes, and leave_match() forgets the old id. Mid-match resyncs — undo restore, reconnect — never arm it, so they still cannot replay the ceremony, which is the only thing the id check ever existed to prevent.

smoke_battle_roll now plays a SECOND battle from scratch and asserts the reveal comes back (CHECK 36-39). Verified by reverting the fix: the new checks fail, which is the point of adding them.

Changed — gun power trimmed ~20%

Guns ignore the wielder's stats entirely, which is why they carried the game's highest WP ladder (4-12, against swords' 2-8). That flat rating means a squishy caster hits exactly as hard as a knight, and Dual Wield lets them do it twice. WP is now 3/4/6/7/9/10/10 across the ladder — tier ordering and gaps preserved. Trimming WP is the lever that touches only guns; re-introducing a stat term would undo the point of the class.

Added — infrastructure

  • AbilityDef.aoe_shape (Radius | Line) and AbilityDef.status_duration, both serde(default). Shape is kept OFF AbilityType so effect and footprint stay independent.
  • Generic DamageAndDebuff(id), InflictRandomDebuff, InflictWithFaith(id) ability types — new status abilities stay pure data rows.
  • quickmatch_six_players.rs: the 3v3 one-unit-each queue (6 seats) had never been tested; only the 2-player army queue was. It works — the sixth player tips it and everyone collects the same match id.