Pokémon details screen — scrollbar-lane fix + consistency / cleanup pass
Twilight: the Pokémon details screen is one of the last carried over from the 2020 era; check whether it's been brought up to the standard the other screens slowly established, then do a full cleanup. Audited it against reference/ui-patterns.md — structurally it's where most of those conventions were born (option-#2 shaded labels, borderless combos, bare IconButtonSquare ⋮, slider tooltips), but it predated two later ones.
What was off
- General tab ⋮ menus under the scrollbar. OverviewTab.qml's scrollable ColumnLayout used width: scroller.availableWidth (no - 16), so the right-aligned ⋮ buttons on the Nickname / OT Name / OT ID rows sat under the Material overlay scrollbar and were hard/impossible to click — the exact "Scrollable forms" gotcha. (StatsTab/MovesTab aren't scrollable; GlancePane is anchored — none of those collide, so only the General tab needed it.)
- No height knobs. The tab's controls sized to each Material control's natural height, so rows read as inconsistently sized (the "oddly sized rows" the textH/comboH knobs exist to fix).
Changes (QML only)
- OverviewTab.qml: ColumnLayout width → scroller.availableWidth - 16 (reserve the scrollbar lane); removed the Exp slider's magic Layout.rightMargin: 25 so it aligns to that same lane; added property int textH: 30 / comboH: 38 and applied Layout.preferredHeight to the text fields (OT Name / OT ID / Catch Rate) and combos (Type ×2, Future Nature). Left the self-sizing nickname NameDisplay alone.
- GlancePane.qml: removed a dead commented-out width block on statsData; collapsed two identical-branch ternaries (the HP slider's to: and the track color:) to the plain getTo() / getColor() calls — behaviour-identical.
- PokemonMoveSel.qml: "Corect Move" → "Correct Move" in the move overflow menu.
Verify / ship
- tst_qml_screens 17/17 (pokemonDetails loads clean through the real engine); full ctest 66/66 (tst_gui_drag confirmed green on its own after a launcher-PATH false alarm). Screenshots (tmp/screenshots/editor/pokemon_editor_general.png) confirm the ⋮ dots now inset off the right edge, rows consistently sized, slider aligned, GlancePane unaffected. Kit app relinked for in-app review. VERSION 0.8.14-alpha → 0.8.15-alpha (PATCH).
Market: split "Normal Items" into Normal + Unbuyable; rename Game Corner "Inventory"
Twilight: split the Pokemart's "Normal Items" into items you can buy and sell ("Normal Items") and items you can sell but not buy — i.e. priced/given in-game but not stocked by any store ("Unbuyable
Items"). Verify against pokered that anything we exclude truly has no buy or sell price in any currency (don't list an item with no discernible price). Rename the Game Corner "Pokemon Inventory" → "Inventory" and apply the same Normal/Unbuyable split there too.
What pokered says (the oracle)
- data/items/prices.asm — the per-item buy price (sell = half). Price 0 = cannot be priced/sold. Zero-price items in our data: Master Ball, Moon Stone, Exp All, PP Up, Ether, Max Ether, Elixer, Max Elixer; plus everything with no price field at all (Town Map, Bicycle, fossils, rods, key items, HMs…).
- data/items/marts.asm — the buyable set = the union of every referenced mart's stock. The two unreferenced clerk texts (the bike shop; a stray spare mart) were excluded — never reachable, and every item they list is sold elsewhere anyway. The reachable union is 36 normal items + 9 TMs (Celadon counter: TMs 01,02,05,07,09,17,32,33,37).
Change (C++ only — no JSON, per the data-edit rule)
- ItemMarketModel::buyableInMart(ItemDBEntry*) — new predicate; a static QSet<int> of the mart-stock item indices transcribed from marts.asm (indices are stable game item IDs, so no DB/JSON data is needed). Each entry is commented with its item name; the 9 buyable TMs are called out.
- buildMartItemList() rewritten: the priced normal items (the existing vendorListItem price-gate) are partitioned into Normal Items (buyableInMart true) and Unbuyable Items (false), each sorted and listed under its own header. Items that fail the price gate (price ≤ 0 in the active currency) land in neither — exactly the "no way to buy or sell it, don't show it" rule. Refactored the repeated sort/append into two local lambdas (sortByName/appendItems); Glitch Items section unchanged.
- Rename: the coins/Game-Corner header "Pokemon Inventory" → "Inventory". The Normal/Unbuyable split runs in both money and coins modes, so the Game Corner gets it for free (buyability is intrinsic to the item, independent of currency).
- Verified exclusions: the excluded zero/absent-price items return 0 from all four price accessors (buyPriceMoney/Coins, sellPriceMoney/Coins) — genuinely no price in any currency, matching prices.asm.
Tests / ship
- tst_market_model (12) green, tst_qml_screens (incl. pokemart) green; full ctest 66/66. The market test asserts on row types/totals, not section names, so the new sections didn't perturb it.
- Kit app rebuilt + launched for a visual pass. VERSION 0.8.7-alpha → 0.8.8-alpha (PATCH).
Follow-up: final grouping — Normal / Vending Machine / Special (all buyable+sellable in-editor)
A few quick iterations with Twilight landed the model. Two corrections to the first attempt: (1) the vending-machine drinks (Fresh Water / Soda Pop / Lemonade) are buyable in-game — at the Celadon-roof vending machine (data/items/vending_prices.asm), not a script_mart, which is why a marts-only set mislabeled them. (2) The framing: every priced item is buyable and sellable in the editor (a buy price, a half sell price, and a coin/money equivalent are all inferable); the sections only describe how the real game treats the item. So the "Unbuyable" name was wrong — renamed Special Items.
Final buildMartItemList grouping of the priced, non-glitch, non-once items (each still fully buyable + sellable here):
- Normal Items — a Poke-Mart shelf sells it in-game (buyableInGame() true via marts.asm).
- Vending Machine — the three roof drinks, pulled out first via isVendingItem() (indices 60/61/62); buyable in-game but a separate vendor, so its own group.
- Special Items — priced but no in-game vendor sells it (Nugget, Rare Candy, Max Revive, the non-Celadon TMs).
buyableInGame() carries the mart+vending index set (transcribed from marts.asm + vending_prices.asm); isVendingItem() carries the three drink indices. Code-only (no JSON). Exclusion unchanged: items with no price in any currency (prices.asm 0/absent) are listed nowhere. Game Corner header stays "Inventory".
Open thread (not done): Game-Corner-exclusive items have a coin price but no money price, so they're coins-only (shown under "Inventory"). A strict "sellable anywhere" reading would give them an inferred money price too — flagged for Twilight rather than changed unprompted, since it touches ItemDBEntry pricing and the coin-only framing may be intentional.
tst_market_model + tst_qml_screens green. VERSION 0.8.8-alpha → 0.8.9-alpha (PATCH).
Follow-up: Sell list hides truly un-sellable items, keeps free ones
Twilight: the Pokemart Sell view still listed items you can't sell. buildPlayerItemList was appending every Bag/Storage slot. Added a gate — Item::canSell() (price >= 0).
- First cut gated on sellPriceOne > 0, which was too aggressive: it also dropped free items. Twilight flagged it — Master Ball is price: 0 and is still sellable in this model (sells for nothing); only items with no price field at all (canSell() false — Town Map, HMs, badges, key items) genuinely can't be sold. Switched the gate to canSell().
- "Free items shouldn't be buyable" is already satisfied on the buy side: vendorListItem requires a buy price > 0, so price-0 items never land in the Normal/Vending/Special buy groups. They appear only in the Sell list (when owned), which is the intended one exception to "priced ⇒ buyable + sellable".
- tst_market_model + tst_qml_screens green. VERSION 0.8.9-alpha → 0.8.10-alpha (PATCH).
Accent sweep: "Poke" → "Poké" everywhere user-visible (+ US-English note)
New standing rule (saved to memory): plain "Poke" is a typo; always Poké / Pokémon / Pokédex / Poké Mart / Poké Ball …, in and out of the app.
- Display-only sweep. Changed user-visible strings, never identifiers. QML: a quoted-string-aware PowerShell pass (replaces brand words inside double-quoted substrings, skipping qrc:/.qml so component refs like PokemonDetails are untouched) over 13 .qml files — Home tiles, the Market venue label ("Poké Mart"), ~20 qsTr help strings. C++: router.cpp 3 screen titles. Data: 4 items.json readable names. Left alone: uppercase internal name keys, route ids ("pokemart"), *.qml filenames, the C++/QML type-registration names, and an external CC artwork title in About.qml.
- The trap — readable is also a link key. tst_sprite_data SEGFAULT'd after the rename. Cause: MapDBEntrySpriteItem::deepLink() resolves a map's ground item via ItemsDB::getInd().value(item), and that index is keyed on name and readable. maps.json had "item": "Poke Ball", so renaming the readable orphaned the link → null deref. Fixed by updating that one maps.json reference to "Poké Ball" (verified it's the only data ref to any renamed item; the many "pokedex": N hits are dex numbers, not the item). Lesson for later: map names ("Pokémon Tower", "Pokécenter") have the same double-duty and a whole-maps.json rename would need the toMap link keys updated in lockstep — deferred (Maps screen is disabled). tst_bridge title assertion updated to "Pokédex".
- US English: noted in the README (status caveat + "What it is") that the editor targets the US English release of Red & Blue specifically (also saved to memory).
- Open: the app's own name "Pokered Save Editor" (portmanteau, from the pokered disassembly) — accent to "Pokéred"? Left for Twilight's call.
- Full ctest 66/66. VERSION 0.8.10-alpha → 0.8.11-alpha (PATCH).
Detailed item tooltips — vertical slice (the deferred big feature)
Twilight pointed out I'd kept deferring the per-item detailed-tooltip feature. Built it end-to-end as a working slice:
- Data: new optional info string in items.json → ItemDBEntry::getInfo() (Q_PROPERTY).
- Market plumbing: ItemMarketEntry::infoText() (virtual, default ""); StoreItem/PlayerItem override to return their DB entry's getInfo(). New InfoRole / dataInfo on ItemMarketModel. Proxies (view/cart) forward it automatically.
- UI overhaul: new DetailToolTip.qml — info badge + bold title over a wrapped body, dark rounded bg, gated on hover + the header "?" toggle + non-empty text. Wired onto the Market list rows. (Old plain MainToolTip left in place for the simple hint texts.)
- Content (first 11): Special (Nugget, Rare Candy, Max Revive), Vending (Fresh Water/Soda Pop/Lemonade), priced Glitch (Safari Ball, ITEM_32 "PP Up 2", Coin — code-level explanations), Normal examples (Potion, Ultra Ball). US English, accented, pokered-checked.
- Still to do (next): author info for the rest of the items; add auto-derived facts (teaches/found-at from toMove/toMapSpriteItem); wire the tooltip into the Items/Bag and other screens where items show.
- tst_market_model + tst_qml_screens (loads the new component) + full ctest 66/66. VERSION 0.8.11-alpha → 0.8.12-alpha (PATCH).