v0.5.22-dev
Four new battlefields traced from FFT maps, summons that arrive instead of being thrown, and every skill and item now states what it does.
Four new battlefields, built to be a testing ground for the height rules —
two recreations of real Final Fantasy Tactics maps, each with a compact
sibling.
The Highspire pair
highspire_canals (10x15, 4v4 + 5v5) recreates Final Fantasy Tactics' Magic
City Gariland — ffhacktics map #22 — and highspire_quarter (8x10, 2v2 + 3v3)
is its compact sibling, the same relationship Potter's Field has to Cursed
Graveyard.
What is exact and what is not, because it decides how much to trust the
numbers. The 10x15 footprint is exact, read off the source map's own axis
labels. The feature layout — canal system, house blocks, cobbled streets,
grass fringe, the town stepping up toward the north-west — is traced from the
top-down render and the four isometric views. The per-tile heights are
interpreted from those renders, not extracted from the game's terrain table,
which is not published anywhere reachable. Every elevation is faithful to the
silhouette rather than byte-exact, and is expected to be tuned.
They are deliberately not mirrored. Every other showpiece battlefield in
the catalog is symmetric so neither side inherits an advantage; a real town is
not. These are a height testbed first and a fair fight second — keep them out
of ranked pools until they have been played.
Why they exist
Roofs are authored as walkable staircases, so the same rooftop is four
steps for a Jump-1 unit and two for a Jump-2 unit. Roof edges then drop three
or four at once. Getting down is a different question from getting up, and that
asymmetry is the thing the height rules most need exercising against.
The stone glyph ladder grew to cover it: s/S/t/T/k are heights 1
through 5. Stone at new heights rather than a new terrain type on purpose — a
tiled roof is masonry, and the client mirrors terrain names but takes height
as a plain integer, so new heights cost zero client edits where a new terrain
would add another hand-maintained mirror to drift.
To read a board without booting the game:
cargo test traced_map_height_table -- --nocapture
It prints both maps as numeric elevation grids, north-up in the same
orientation as the source renders. A glyph is compact to author and useless to
check a movement calculation against.
A test that could be satisfied by flattening the map
traced_maps_are_a_walkable_height_ladder asserts every level 0–5 is
present and every passable tile is reachable by a Jump-1 unit — no roof is an
island. Verified by stranding the tower peak, which fails it by name and
coordinate.
But a completely flat map passes that test perfectly while destroying the only
reason these maps exist. So it ships with a counterpart,
traced_maps_have_ledges_that_reward_a_bigger_jump, pinning the steep ledges the
first test would happily let you delete (25 live, floor of 8).
The reachability flood is a local implementation, deliberately not
movement::get_move_range: it asserts the map is authored correctly, not that
the mover works, and sharing one implementation would let either bug hide the
other.
The Gatestone pair
gatestone_bastion (10x13, 4v4 + 5v5) traces Zaland Fort City — ffhacktics
map #35 — with gatestone_ward (8x10, 2v2 + 3v3) as its compact sibling. Same
provenance rules as above: exact footprint, traced features, interpreted
heights, not mirrored.
It is a different shape of height problem from Highspire, which is the
reason to have both. Highspire is a town: staircases everywhere, many short
climbs, height as texture. Gatestone is an assault — one tiered keep climbing
0 to 5 straight up the middle, a curtain wall across the waist with a single
gate, and a low open approach to the south. The gate is the bottom of the
staircase, so the chokepoint and the climb are the same decision.
Both armies start in the low south; the spawn columns run only the bottom six
rows. The high ground is north of both of them — contested rather than
owned, which keeps it a fight instead of a siege.
All four maps are now walked by one list, tests::TRACED_MAPS, so a fifth is
covered by adding it there and nowhere else. traced_maps_keep_their_traced_footprints
additionally pins each pair's signature feature — the canal for Highspire, the
curtain wall for Gatestone — because a map authored out of its own defining
feature is a different map.
A town theme, built out of two packs
All four traced maps are dressed with a new town theme. It spans two asset
packs on purpose, because neither can dress a settlement alone: Dungeon
supplies the only masonry we own — paved tops for streets and roof terraces,
and real walls — while Forest Nature supplies the only vegetation, for the
grassy banks that ring both source maps in their own renders.
Mixing packs is safe because scatter and structure entries take a per-entry
scale and only fall back to the theme's. The theme scale is the Dungeon one,
which is what the floors and walls need, and every Forest prop carries its own.
The two packs are authored at wildly different unit sizes — a 4-unit grid
versus roughly 1 unit to the metre — so one shared number could never have
served both.
Grass deliberately gets no surface, the same call the forest theme makes:
paving the banks would erase the one part of these maps that is supposed to
read as outdoors. Stone scatter runs about 13% against the dungeon's 37% and
drops the torches and banners — a street should read as a street, and a wall
sconce outdoors reads as a mistake. Water is daylight blue rather than the
dungeon's near-black, which read as a pit rather than a canal you are meant to
wade.
This is not yet a real town set: there are no tiled roofs, no
half-timbering, no cobbles, no market clutter. The roofs are flagstone terraces
because flagstone is what we own. Worth revisiting when a village pack lands —
the swap stays a per-map edit in MAP_THEMES plus the one theme entry.
(For the record, since it came up: the Forest Nature FREE tier is props
only — 105 models, every one a rock, bush, tree or grass tuft, with no
floors, walls or buildings. Verified against the distribution zip. That is why
it can't dress a town by itself, and why the forest theme has no surfaces
either.)
The game now says what your skills and gear actually do
The roll ceremony named the gear it dealt you. The results board named both the
kit and the loadout. Neither said what any of it was for — you watched an
ultimate weapon turn over with no way to know why it beat the one it replaced.
On the roll screen the stats sit beside the name. The card went from 240 to
320px, which was a measurement rather than a guess: across all 241 items a stat
line runs 11 characters at the median and 41 at the worst ("4 WP, +22 MP, +6
MA, +3 MDEF, +9 EV, Holy"), so 240 fit roughly the shorter half. At 320 the
great majority land on one line and the handful of outliers wrap instead of
truncating — no per-item decision, and no item quietly losing its last stat.
On the results board every chip hovers. Skills, gear and the ability-usage
line each carry a tooltip with the mechanics, the base hit chance and the
prose. The board is 440px with up to eight units a side and three dense lines
each, so printing mechanics inline for six skills and five items would bury the
stats the board exists to show. Hover costs no layout at all.
Weapons now state their base hit % — 80% for an axe, 100% for a dagger.
Accuracy has been live since v0.5.10 and had never been shown anywhere, so a
fifth of an axe's swings went missing with nothing to explain why. It appears
on the roll card, in the hover, and on the Codex's weapon-class pane.
One sentence, one author
EquipmentDef::stat_mods_label() has always existed in the engine — the
Discord roll bot renders it verbatim — and the Codex derived the same sentence
a second time in GDScript. Two more screens wanted it, which would have made
four authors for one summary. It now ships on the item itself and every screen
reads it.
A guard for the numbers being printed
smoke_phase2 compared ids, names, rarity and JP cost. It never compared
mp_cost, ct_cost, power, range, aoe or accuracy — precisely the numbers these
screens now print, out of a mirror that is maintained by hand and has drifted
in six documented ways.
It found a real bug on its first run. Chakra has cost 10 MP since v0.5.10 and
the mirror still said 0, so for eleven releases the client showed it as free
and let a monk with no MP select it, whereupon the engine refused. That is the
"Chakra is broken" playtest report v0.5.15 built CommandRejected to explain —
with the half that was actually lying still sitting there.
The engine serialises AbilityType as its Rust variant name while the client
keeps its own vocabulary, so comparing them field-for-field would need a
twenty-row hand-kept mapping — the disease rather than the cure. What is
guarded instead is the single fact the displayed accuracy depends on: the set
of abilities the engine calls MagicDamage must equal the set the mirror calls
"magic". Verified by diverging one (48 vs 47).
Two bugs the tests caught by being reverted
weapon_type is an Option engine-side, so String(null) did not return ""
— it aborted the call and yielded a coerced 0, which every helm would have
rendered as "0% acc".
And the accuracy test was vacuous: it searched every tooltip for "95%" and
passed with the ability-accuracy path stubbed out, because the Kingsworn gear
hover states 95% by its own separate route. It now asserts against ability
hovers only. Same lesson an_archer_fires_from_bow_range recorded — to test an
inherited value, strip out every source that isn't the inheritance.
Summons arrive, and an ability's numbers reach its effect
The plan was to hand-author the nineteen ultimates and the summons. Walking
them produced two derivation rules instead, which is the same result the FX
pass's phase 4 got when it set out to write twenty-three overrides and came
back with three.
A summon is not thrown
All sixteen Eidolon Caller rows derived as bolt — the archetype for
something the caster throws. So the summoner lobbed Ifrit like a fireball,
which is the one thing that class must not look like: an eidolon arrives
under its own power, and the summoner is a doorway, not a catapult.
A_SUMMON is a seventh archetype, and it opens one gate at the centre of
what the ability hit, not one per target. Every other motion loops the target
list, which is right for four separate fireballs and wrong for one creature —
four eidolons would read as four summons. Four beats, and the order is the
read: the rune opens, a column rises out of the ground (the exact inverse
of rain, which drops one in), a presence flares at the top on the frame the
blow lands, and the element scatters — outward from an eidolon that came to
hurt someone, inward from one that came to help, which is the vocabulary
v0.5.17 established for buffs and debuffs.
It is keyed on job == "summoner", which is honest input here in a way it
would not be for a knight: all sixteen of that job's action rows are
summons, so the job is the fiction rather than a proxy for it. Summon
seventeen is dressed on registration with no edit anywhere.
Bahamut's override lost its archetype key — it had been forcing rain only
because a summon derived as a bolt, and the King of Dragons arriving is this
motion at its largest.
Twelve Arcanist spells drew three effects between them
The archetype said how a spell moves and the palette said what it is made of.
Nothing said how big it is. So fire (power 12, aoe 1) and cinderfall
(power 40, aoe 2, "the sky rains ash and ember over a wide field") derived
byte-identical plans, as did Ifrit and Salamander, Titan and Cyclops, Lich and
Odin.
Two new axes, both read off the row the client already holds:
footprint(fromaoe) sizes the rune, for every archetype. It is
suggestive rather than literal — the engine's AoE is Manhattan, a diamond,
and the rune is a circle, so a rune at the exact radius would claim a
footprint the resolver does not use. The targeting highlight already owns
the true shape.magnitudemultiplies every sprite in every motion, and reads the
larger of what an ability does and what it cost. That second half is not
a garnish: a status ability haspower: 0by design, so reading power alone
put eight of the nineteen ultimates on the size floor together — Death
Knell, 600 JP and ultimate rarity, drew smaller than a common Fire. Across
the damage catalog power and MP track each other closely enough (12/8, 30/24,
50/45, 60/55) that only Ultima moves, by 0.08, because it costs more to
throw than it hits for.
Elixir is the single genuine override to come out of the ultimates: power 0
and mp_cost 0, because FullRestore scales off nothing. There is no number
to read and no rule would have found it without inventing one for the row.
A seam for hand-drawn effects
SUMMON_ART is an empty, documented table. An ability id maps to a
Texture2D — which is what Final Fantasy Tactics itself does, a painted plate
rather than a model — or to a PackedScene. An absent id gets the abstract
bloom, so the art can land one summon at a time and a half-filled table is not
a half-broken game. Position, arrival frame, snap-in and burn-off are already
computed and already scale with magnitude.
The one hazard is written at the branch that handles it:
GeometryInstance3D.transparency is 0-is-solid and Sprite3D.modulate.a is
1-is-solid, opposite directions, and it is the line most likely to be got
backwards on the day the art arrives.
Billboarding was discarding every scale animation
billboard_keep_scale was never set, and without it Godot's billboard shader
rebuilds the model-view basis from the camera and throws the node's own scale
away. Every scale tween in the FX layer — the impact sprite's pop-in from 0.4,
the sweep's stretch along its axis — has been rendering at a flat fixed size
since the art phase shipped. The calling code always meant otherwise.
Previewing
scenes/test/fx_preview.tscn plays any ability on a real board, on a loop.
Arguments are classified by what they are rather than by position or a flag:
an ability id plays that ability, a job id plays that job's whole action list,
a rarity plays every ability of that tier, a map id is the battlefield. So
++ summoner or ++ ultimate reviews a school smallest-first, and collisions
land adjacent instead of eleven entries apart.
Guards
smoke_fx_plan 52 and smoke_fx_art 144, and the load-bearing one is stated
as an outcome — no ultimate draws at the size floor — not as "magnitude
reads mp_cost". The outcome is the thing that was wrong, and a later axis could
floor them again by a different route.
Verified by reverting: killing the summon rule fails 2, the charge threshold 1,
a constant magnitude 4, the mp_cost half 1 (naming all seven ids), and dropping
* mag from a single motion fails 1. That last one is the interesting number —
with the bolt's head unthreaded it still grew, 0.52 to 0.54, because its trail
sprites scaled. A "did it get bigger?" assertion passes against it, so the check
demands 1.5x.
A tier check was vacuous on its first pass and only reverting found it.
Bahamut is hero-tier by rarity, so counting it reported a healthy split
across the summon school while the charge threshold did nothing whatsoever.
Excluding ultimates from that check is the whole of it.
Not fixed, and why
Ifrit and Salamander remain 4% apart — same element, same area, same tier.
No difference was invented to hide it. Salamander's own row says "the
fire-drake coils and breathes", which is a breath weapon: one
shape(db, "salamander", Line) in the engine and it derives to sweep
automatically, becoming a different summon with no FX code at all. Odin's
"rides down the line" suggests the same. Both are content calls.
Persuade derives as a bolt — an Orator talking renders as a thrown
projectile. That is not an override, though: the mediator's entire kit is talk
skills and all of them derive as bolts. It is the summoner situation exactly,
and it wants a rule.
Install: download the zip, extract the whole folder, run shattered-crowns-windows.exe.
Keep the sc_engine folder next to the exe - the game will not start without it.