|
Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
|
Newest first. The keyboard redesign is the last thing that happened today.
Twilight: "caps lock and holding down shift are 2 ways to get page 2, so why are there different page 2 depending on caps lock, i think something got crossed."
Nothing was crossed — but the model was wrong, and her confusion was the bug report. Caps-affects- letters-only (the physical-keyboard rule I'd just carefully implemented) produces a layer that is not one of the 8 pages: uppercase letters over an unshifted number row. The page strip therefore couldn't name it, so the deck looked like it was lying about where you were. A model the UI can't display is a bad model, however correct it is. Caps now locks the Shift page entire — Shift inverts it, Ctrl/Alt ignore it — and every state the deck can be in is exactly one page again. The trade (the punctuation row rides along, so a digit means tapping caps off) is visible and rare; digits aren't in-game-legal in a name anyway.
"colour was supposed to be visible at a glance, not only on mouseover", "keyboard looks really bad but i cant place my finger on why", "the colours on the top and bottom are really ugly and the top and bottom bar is way too tall", "you couldnt see the key text upper right corner", "couldnt you fill in at least the regular keys around the keys you drew". All fair. Every cause had a name:
ctest green (74/74). v0.16.2-alpha.
Twilight, on the finished deck: "its counterintuitive to have shift lowercase, i get what you did you did exactly what i asked you but i only just now realized that little thing. a-z should be without shift and uppercase with. Besides that dont just do a quick hotswap, you may actually have to replan things."
She was right, and the reason matters. I'd put uppercase on the base layer because Gen 1 names are all-caps — which sounds like good reasoning until you notice it makes the deck contradict every keyboard its user has ever touched. "Gen 1 names are uppercase" is an argument for a good Caps Lock, not for inverting the alphabet.
And it genuinely wasn't a hotswap. Flipping the two letter blocks is trivial; what it forces is:
The tempting shortcut — "Caps = latch the Shift page" — is wrong, because the Shift page also carries the punctuation row: with caps on you couldn't type PIKA2 without unlocking. So Caps Lock is a real Caps Lock, with the three rules everyone already knows:
That means the deck now honestly shows two pages at once — uppercase letters above a digit row — which is precisely what a keyboard does. The rules went into C++ (FontKeyboard::pageForKey) so tst_font_keyboard could pin every one of them, rather than being three ifs buried in a QML binding.
One real wrinkle: Qt gives no portable way to read the caps light. If it was already on before the screen opened, the deck would start out of step. The fix isn't a hack — event.text is the OS's own answer (it has already folded in caps and shift), so the first letter you press tells the deck the truth and it corrects itself.
Also from Twilight: physical modifiers must not latch — hold Ctrl, the page flips; let go, it drops back. Clicking an on-screen modifier cap (or a page button) still latches, because a mouse can't hold a chord and click a key at the same time — and because a latched page is the only way in when the OS eats the chord.
The old "click into the box and the deck quietly stops listening" was a hidden mode. It's now explicit:
The header names the mode in words, so nobody has to infer the rule from an animation.
The edit-mode shot came back showing keyboard mode. The screenshooter had found the item by its editMode property — but NameFullHeader re-exposes that property and, being an ancestor, was found first; invokeMethod("beginEdit") on it did nothing and the shot silently lied. Finding it by its method instead fixed it. (A screenshot harness that can quietly photograph the wrong state is worse than no harness, so: find things by what they can do, not by a property name that may be re-exported.)
Also from the shots: the detail pane's idle hint elided its last line into a trailing "..." that read as a bug — cut to three single-spaced lines that fit the 175px pane.
ctest green (74/74). v0.16.1-alpha.
Twilight: "i want to totally redesign the fullscreen keyboard screen its pretty bad and its never been good." Her idea, in her words: an outline of a real ASDF keyboard, each key holding a tile instead of a letter, with the key it's bound to printed superscript in the corner so you can either click it or just type it. Pages via Shift/Ctrl/Alt combinations, page buttons above, a colour reference where the category filters used to be, no tilemap, no Grid/Tileset toggle. And: "intelligently map out symbols to keys dont just dump them in order... do a comprehensive planning first."
So the plan came first: plans/full-keyboard-redesign.md.
255 tiles. 36 alphanumeric keys (26 letters + 10 digits). That needs 8 pages — and Shift/Ctrl/Alt give exactly 8 combinations. One page per chord, nothing left over, nothing arbitrary. The 255th tile (Space) rides the real spacebar, where every human already expects it.
Not a linear fill. Three rules, in priority order:
Pages run in the category order, so the four cheapest chords hold every Normal, Single-Char, Multi-Char and Variable tile — everything you'd ever legitimately put in a name — while Pictures and the Control codes that glitch a name sit behind two- and three-key chords. You cannot hit <end> by accident.
The map is C++ (mvc/fontkeyboard.cpp) and tst_font_keyboard (39 cases) pins it. The failure modes here are all silent: a tile on two keys, or — far worse — a tile on no key, unreachable forever, which would never surface as a crash or a warning. The test asserts every tile 1–255 appears exactly once across the 8 pages + the spacebar, that no page collides or overflows, that every code resolves in FontsDB, and the identity contract (page 1 A is A, Shift A is a, Ctrl B is bold B…).
It immediately earned its keep: pageFor() returns a modifier MASK (shift 1, ctrl 2, alt 4), so Alt is page 4, not "the 4th page" — my page table was indexed by reading order and had silently swapped two entire pages. Now the index is the mask, and the human reading order is a separate pageOrder that only the page strip uses.
Both now in reference/qt-patterns.md.
Screenshot review found four real defects that no test could:
Also spotted from a shot: switching page under a stationary mouse left the detail pane describing the tile that used to be under the cursor (no enter/exit fires) — KeyCap.onInfoChanged re-announces now.
Every image://font request rebuilds the whole tileset. 36 animated keys × an 8-frame cycle would have melted the UI — the same cost that once froze the hover tooltip. So the deck requests the whole 16×16 tile sheet once per frame and each key clips its own 8×8 cell out of it; same URL for every key, so QML's pixmap cache hands out one pixmap. One timer on the deck drives all 36 caps.
Backspace deletes a whole <code>, never one character out of the middle of one. A key that won't fit shakes the name field instead of silently doing nothing. The name box stays a real text field (click in for Ctrl+C/V/Z; the deck dims its key legends to show it's handed over the keys). The category filter became a colour legend — the deck shows everything, so there's nothing left to filter. Gone: the chip list, the filter sidebar, the raw tilemap view, the Grid/Tileset toggle, and FontSearchModel.
Full ctest green (74/74, including byte-fidelity and visual regression). v0.16.0-alpha.
A long UI-and-economics session on the Market: finished the trainer-card polish, built the Exchange sub-tabs (Currency / Healing / Custom) with real item<->item trading, reworked the dropdowns after feedback, found and fixed a serious pricing bug on the way out the door, shipped v0.15.2-alpha, and finally gave the exchange the unit tests it should have had before it shipped.
Also a batch of standing process changes from Twilight (manual releases, mandatory screenshot review, notes-over-memory, a fast-dev harness loop) – all now written into CLAUDE.md and the notes rather than living in an AI's head.
The item exchange priced the trade per step: every item you asked for was separately rounded up to a whole number of the item you were paying with, and each one refunded its own leftover as money.
Twilight caught it with one sentence: "If they're asking for 3 fresh water at 200 each then 2 potions can be spent exactly and no refund is necessary." She was right, and the old code was badly wrong:
| old (per step) | new (whole trade) | |
|---|---|---|
| 3 Fresh Water (₽600) paid in Potions (₽300) | 3 Potions spent, ₽300 refunded | 2 Potions spent, ₽0 refunded |
| 3 Fresh Water paid in Super Potions (₽700) | 3 Super Potions, ₽1,500 back | 1 Super Potion, ₽100 back |
The fix: round the total up once, not each step, and refund only that single leftover. An evenly dividing trade now costs nothing extra and refunds nothing – and the rate line under the button drops the +₽0 rather than advertising a refund that isn't happening.
perAGive / perARefund / perBGive / perBRefund (the four per-step rates) were replaced by giveFor(dir, steps) / refundFor(dir, steps). The preview, the "+" gating, and checkout() now all call the same pricing functions, so they structurally cannot disagree about what a trade costs.
Process lesson: this shipped-adjacent bug was in the code for two commits and survived a live screenshot review, because the screenshots looked fine – the numbers were self-consistent, just wrong. Eyeballing a UI does not verify economics. The unit tests written at the end of the session (below) are the actual guard, and they should have existed before the feature was released, not after. See notes/plans/testing.md.
The Market's Exchange tab now has a sub-tab bar:
Each side is an item dropdown + a live count + a + button; steps accumulate, the after-counts and money are previewed, and nothing is written until Checkout. Values come from each item's money buy price (Twilight's call: it's the price players see in-game most often, so it reads as fair). Items are counted and written across the bag + PC storage combined.
Driven by a new ItemExchangeModel (brg.itemExchangeModel) over a single net axis – the same shape as the money<->coins coin axis it sits beside. Backed by new ItemStorageBox helpers (capacityForInd() / addAmount() / removeAmount()).
First cut restricted both dropdowns to items you already own, which made the feature far less useful than it should be. Reworked so the sides are asymmetric:
The catch: listing every item would be a trap if you could select one you can't pay for. So the get list flags what your stock can't cover and the dropdown greys those out (with 25 Antidotes = ₽2,500, Calcium / Carbos / Full Restore render dimmed and unselectable).
That greying is not just polish – it's what makes Twilight's "never have a case where both buttons are greyed out" rule structural rather than a special case: anything you can select is by definition affordable, so its "+" is always live. Once a step is queued the opposite "+" comes back too, so you can always walk a trade back.
The Healing default prefers the potion family in strength order among the items you actually hold (Potion > Super > Hyper > Max > Full Restore, then the rest) – a save with 8 Super Potions and no Potions opens on Super Potion, never on Antidote just because it sorts first.
This is where the mandatory-screenshot-review rule came from. The playtime clock was overlapping the trainer artwork and it shipped, because I didn't actually look at the render. Manual screenshot review is now a by-default step on any UI change – see CLAUDE.md and reference/ui-patterns.md.
New tst_item_exchange (14 cases, projects/tests/mvc/tst_item_exchange.cpp), pinning the parts a user cannot verify by eye and that touch their save:
A priceData_isWhatTheseTestsAssume() guard asserts the Gen 1 buy prices the worked examples rely on, so a future items.json edit fails loudly there instead of confusingly inside the arithmetic.
Full ctest green.
v0.15.2-alpha (main @ ac79c0a) – release, pages, tests and lint all green; GitHub Release published. The tst_item_exchange suite landed on dev immediately after and rides the next release.