Skip to content

Feature set 174. - #89

Merged
jackfperryjr merged 2 commits into
mainfrom
features-174
Aug 11, 2026
Merged

Feature set 174.#89
jackfperryjr merged 2 commits into
mainfrom
features-174

Conversation

@jackfperryjr

Copy link
Copy Markdown
Owner

Enemy stat coverage: the parser now reads the per-game fields it was missing, and the battle model no longer misreads a game whose attack and defence are on different scales.

Why

Whole stat columns were empty for whole games, and it was never "the wiki doesn't say" — it was field names the parser didn't speak. Sampling six popular enemies per game and diffing the real field names against the alias lists found the gap game by game.

The data pass that came out of it filled 2,566 null cells across 951 rows and corrected 1,488 more where the parser disagreed with what was stored. Monsters now hold 37,840 non-null values across the 16 stat columns, up from 35,276, and residual wiki markup in stored values is down from 7 cells to 0.

Commits

Read the per-game enemy stat fields the parser was missing

Field names match a hyphen or underscore wherever they carry a space (Final Fantasy V's template was migrated to magic-defense / drop-1 / ps-abilities in 2026, which is why every FFV enemy read as having no magic defence, no drops and nothing to steal). Platform tags take the same separator, the min forms of attack and defence are read the way hp min already was, and FFXII's numbered magickname/technickname fields are collected.

vitality and spirit are scoped per game rather than added to the global lists. They are the defence stats in Final Fantasy XV, whose template carries nothing else — but XII and IX list them beside a real defence, where reading them globally reports a Banshee's defence as 46 when the article says 17. The game is taken from the article's own | release = line, so the parser stays a pure function of the wikitext.

Two CleanFieldValue robustness fixes ride along, both found as markup sitting in stored values. These change what the dashboard's import writes:

  • the leaked-assignment strip took a name of [a-zA-Z_][a-zA-Z_ ]*, and a platform-tagged name doesn't start with a letter, so FFIII's | 3d common drop = survived and was stored as a stolen item
  • the wikilink pattern allowed exactly one pipe, so Abaddon's [[Hi-Ether (Final Fantasy VI)||Hi-Ether]] failed to match and kept its pipes

Put each game's guard stat on the same scale as the offence it meets

BattleMath.Ratio divides offence by guard, which assumes they're comparable numbers. They aren't the same underlying stat in every game, and accurate data exposed it: FFXV publishes strength at a median of 4,080 and vitality at 107, so a median enemy read as forty times stronger than another median enemy of its own bestiary. It had been hidden only because XV published no defence at all and the median-borrow happened to hand it the attack scale.

FFVI has the same fault in the other direction — a small attack against a 0–255 defence — and had every fight pinned at the MinRatio floor. That one predates this work.

FillGapsWithGameMedians now rescales the guard stat so its median matches the offence it's measured against, per game and per stat pair. Scaling is monotonic, so which monsters are tougher than which is untouched.

Game before after backfill after fix
Final Fantasy XV 0.50 3.10 (2 turns) 0.50
Final Fantasy VI 0.20 (17 turns) 0.20 0.50
Final Fantasy IV 2.12 (2 turns) 2.09 0.50
Final Fantasy V (magic) 0.50 1.26 0.50
the other seven 0.66–0.85 unchanged 0.50

Median pairings now sit at parity at about seven turns, and sampling 4,000 random pairings per game still spreads from four turns to seventeen. The client displays ATK/DEF, so those numbers change — FFVI previously showed ATK 13 against DEF 110 and then took seventeen turns.

Sphere Hunter is unaffected: it rates stats as percentiles within a game, so scale can't reach it. It gains 37 draftable spheres because FFXV and FFV monsters used to share one median defence and now vary. Its two documented counts are refreshed.

Kupodle and Triple Triad are untouched. Both pools are HybridCache entries on a 10-minute expiration, so they rebuild themselves.

Testing

560 tests pass, 16 new. dotnet format --verify-no-changes and the Release build are clean.

Before writing anything, a harness parsed all 951 affected articles through WikiClient.ParseMonsterStats and diffed every one of the 2,566 proposed cells against the reviewed set — 0 mismatches. Every write was guarded on the cell still being what the review saw, and revert SQL exists for both passes.

Not in scope

The characters side is still thin (Job 139/2,071), and its release-prefix wildcard is pulling Revenant Wings jobs onto FFXII characters — Ashe reads as a Time Mage. FFVIII, FFXI and FFXIV remain unreachable by aliases: VIII stores level-scaling coefficients rather than scalars, and XI and XIV carry no stats infobox at all.

🤖 Generated with Claude Code

jackfperryjr and others added 2 commits August 11, 2026 16:25
Each game's enemy infobox names its fields differently and the alias lists only
spoke a subset, so whole columns read as empty for whole games. Final Fantasy V's
template was migrated to hyphenated names ("magic-defense", "drop-1",
"ps-abilities"), which left every one of its enemies with no magic defence, no
drops and nothing to steal; Final Fantasy XII states its combat stats only as a
level band and numbers its moves one field at a time.

Field names now match a hyphen or underscore wherever they carry a space, the
platform tags admit the same separator, the "min" forms of attack and defence are
read the way "hp min" already was, and magickname/technickname are collected.

vitality and spirit are scoped per game rather than added to the global lists.
They are the defence stats in Final Fantasy XV, whose template carries nothing
else, but Final Fantasy XII and IX list them beside a real defence, where reading
them reports a Banshee's defence as 46 when the article says 17. The game comes
from the article's own "| release =" line so the parser stays a pure function of
the wikitext.

Two robustness fixes in CleanFieldValue come with it, both found as markup sitting
in stored values:

  - the leaked-assignment strip took a name of [a-zA-Z_][a-zA-Z_ ]*, and a
    platform-tagged name does not start with a letter, so Final Fantasy III's
    "| 3d common drop =" survived and was stored as a stolen item.

  - the wikilink pattern allowed exactly one pipe, so Abaddon's
    "[[Hi-Ether (Final Fantasy VI)||Hi-Ether]]" failed to match and kept its pipes.
    The target is now matched greedily to the last pipe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BattleMath.Ratio divides offence by guard, which assumes the two are comparable
numbers. They are not the same underlying stat in every game, and the enemy stat
backfill made that visible: Final Fantasy XV publishes strength with a median of
4,080 and vitality with a median of 107, so a median enemy read as forty times
stronger than another median enemy of its own bestiary and killed it in two turns
instead of seven. It had been hidden only because XV published no defence at all,
and the median-borrow happened to hand it the attack scale.

Final Fantasy VI has the same fault in the other direction — a small attack against
a 0-255 defence — and had every fight pinned at the MinRatio floor for seventeen
turns, which predates the backfill.

FillGapsWithGameMedians now rescales the guard stat so its median matches the
offence it is measured against, per game and per stat pair. Scaling is monotonic,
so which monsters are tougher than which is untouched; only where the bestiary as
a whole sits changes. A game that publishes no guard stat already borrowed the
offence median, which makes the factor exactly 1 and leaves that path alone.

Every game's median pairing now sits at parity and runs about seven turns, and
matchups still spread from four turns to seventeen depending on who meets whom.
Sphere Hunter is unaffected either way: it rates stats as percentiles within a
game, so scale cannot reach it. Its two counts are refreshed for the pool the
backfill produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jackfperryjr
jackfperryjr merged commit bc6bade into main Aug 11, 2026
1 check passed
@jackfperryjr
jackfperryjr deleted the features-174 branch August 11, 2026 21:28
This was referenced Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant