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 emitStatusResistedwith 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) andAbilityDef.status_duration, bothserde(default). Shape is kept OFFAbilityTypeso 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.