1013 Commits

Author SHA1 Message Date
bryanthaboi c17a8f0d45 Merge pull request #2365 from thibautbus/fix/easy-chat-word-page-rows
Draw the Easy Chat word page's last two words
2026-09-21 14:46:58 -04:00
bryanthaboi ff838c95e6 Merge pull request #2374 from thibautbus/fix/gen3-mod-names-survive-reload
Keep FireRed species and move patches after the pack reloads
2026-09-21 14:46:27 -04:00
bryanthaboi fe28fb8653 Merge pull request #2375 from thibautbus/fix/easy-chat-species-move-names
Let a translation reach the Easy Chat words
2026-09-21 14:45:32 -04:00
bryanthaboi 7da44937a3 Merge pull request #2376 from thibautbus/fix/frlg-summary-move-name-width
Give FireRed summary move names the room pret gives them
2026-09-21 14:45:25 -04:00
bryanthaboi bf7f2bee90 CLOSES #2364, CLOSES #2380, CLOSES #2366, CLOSES #2362 2026-09-21 14:43:32 -04:00
thibautbus 709c7db286 Read the Easy Chat species and move words from the dataset
Four of the 22 Easy Chat groups carry no text of their own: POKéMON,
POKéMON (NAT), MOVE 1 and MOVE 2 are lists of species and move ids, and
pret prints them through gSpeciesNames and gMoveNames.  The extractor
resolves them the same way, so easy_chat_data.lua ended up holding 807
copies of names the dataset already has -- copies that go stale the
moment anything renames a species or a move.  A mod that renames them,
a translation filling the species_names and move_names catalogs first
of all, changed every screen but this one: the picker still offered
the cart's English names, and the profile read back in them.

Resolve those two kinds of word at draw time instead, against the same
Pokemon.name / Pokemon.moveName the party screen and the Pokédex use.
When the dataset has no entry for an id it answers with a placeholder
("POKéMON 063", "-------"), which is worse than the word the cart
shipped, so that case keeps the extracted text.  An entry that names
the group ("easyChat.POKéMON|PIKACHU") still wins over both, since a
species name may need a wording other screens do not use.  A bare-key
entry does not: bare keys are shared with menu labels this engine
already translates (CUT, FLASH, STRENGTH), and one of those should not
decide what a move is called here.

tools/modkit.py keeps emitting those 807 rows, since the species and
move catalogs it writes come from the Gen 1 dataset and do not list a
Hoenn species or a Gen 3 move; its note now says an empty entry shows
the game's own name, so a translator only fills the ones nothing else
renames.
2026-09-21 17:24:41 +02:00
thibautbus bfc2534a81 Let a translation reach the Easy Chat vocabulary
The Easy Chat screen picks its words from src/core/game3/easy_chat_data.lua, the ROM's own word tables as the extractor writes them: 1,813 English words across 22 groups, plus the group names the picker lists. It drew them straight from that table, so every word the player composes with stayed English, and the screen's own prompts -- routed through Strings() already -- sat in a frame of untranslated vocabulary.

src/core/game3/easy_chat_text.lua is the seam. The data module stays exactly what the extractor produced, because it is regenerated from the ROM and anything written into it would be lost on the next import; the words are translated where they are drawn instead, the same split the rest of game3 uses for extracted text. Five sites go through it: the group list, its header, the word list, the word sitting in a slot, and the saved profile a message box prints (ShowEasyChatMessage). The ids the save file stores are untouched.

Each word carries its group's context ("easyChat.FEELINGS"), because the same English word means different things in different groups and 155 of them collide with labels this engine already translates elsewhere (ATTACK, BAG, CANCEL, BACK...). Strings() falls back to the plain key, so a catalog that does not need the distinction lands with one entry -- with the consequence that a mod which already translates BAG for the menus now sees that wording in the Easy Chat list too, until it adds a context-specific entry.

A seam nothing can discover is not much use: tools/modkit.py's catalog generator emits these keys too. The literal harvester cannot see them (the picker looks each word up at draw time, not at a Strings("...") call site), so the dataset dump walks EasyChatData.GROUPS and writes 1,835 rows into lang/strings.lua, where a translator finds them beside the rest of the engine text.

Each word is clipped to the box it sits in: in the picker, the red selection rectangle; in the phrase frame, the slot's own frame. The cart's words fit those boxes (72 px at most); a longer translation is cut at the box edge instead of running past the highlight, into the next slot's cursor, or off screen. Group names get the room up to the scroll arrows instead, because an official translation already needs more than the rectangle: the French cart's VIE QUOTIDIEN. is 83 px.

tests/engine/game3_easy_chat_text_test.lua covers the context lookup, the fallback to a plain key, the untouched English with no catalog, and that the extracted data itself is never rewritten. It lives in tests/engine so the T1/T2 tier actually runs it.
2026-09-21 17:24:41 +02:00
bryanthaboi fdc9506256 another big dump 2026-09-21 11:14:17 -04:00
thibautbus 35727406dc Give FireRed summary move names the room pret gives them
The summary's MOVES page drew each move name with a fixed 64 px limit.
pret prints the names at x 3 of POKESUM_WIN_MOVES_3, a 10-tile window
that starts at tile 20 (pokemon_summary_screen.c:857, :2543), so a name
has up to the right edge of the screen: 77 px from the pen at 163.  The
cart's own longest names are 72 px -- SKY UPPERCUT, FRENZY PLANT, HYDRO
CANNON -- and lost their last letters, as did translated names that the
cart's window would hold (RUGISSEMENT for GROWL).

Clip to the window's right edge instead, measured from wherever the
chrome manifest puts the pen.
2026-09-21 17:09:45 +02:00
thibautbus b54adf14cc Re-apply FireRed species and move patches after the pack reloads
A FireRed mod's pokemon and moves patches are merged onto the live
src.core.game3.pokemon module while the mods load (Game3:_loadMods).
Entering FireRed then runs Pokemon.install() from Runtime, which swaps
every species table for a fresh copy of the ROM pack -- names, stats,
types, learnsets, and the move names that live in the same pack.  The
only reload hook, in Gen3Compat.applyMerged, re-seeded the sprites and
nothing else, so every such patch was gone before the first frame: a
translation's species_names and move_names catalogs never reached the
party screen, the summary or a battle, and a mod's base stats reverted
to the ROM's.

Write the moves and pokemon registries again onto the new tables from
that hook, the way reapplyMoves already does for the moves module.  The
moves registry is part of it because its names and the battle-move copy
it mirrors into belong to the reloaded pack, and it goes first, in the
order Loader:_mergeOrder uses: the moves writer is what adds a mod's own
moves to the move index, and the species writer resolves learnsets, egg
moves and TM/HM lists through that index.
2026-09-21 15:29:01 +02:00
bryanthaboi 2945ed8987 firered: elevators, braille, alt layouts, on-frame scripts, fly and heal data, field item use 2026-09-21 05:39:03 -04:00
bryanthaboi 92b4fb9eb5 firered: forced movement, darkness, safari, rocket hideout, victory road, script ops 2026-09-20 21:17:57 -04:00
bryanthaboi 4d7350c776 Merge remote-tracking branch 'origin/dev' into HEAD 2026-09-20 17:49:32 -04:00
bryanthaboi 1af407498c firered softlocks, specials, cave encounters, sevii ferry 2026-09-20 17:31:31 -04:00
thibautbus 823dd07727 Draw the Easy Chat word page's last two words
The word picker pages eight words at a time and its navigation moves through four rows of two: `row < 3` when moving down, `3 * 2 + col + 1` when stepping up into the previous page. The draw loop rendered three rows, so the seventh and eighth word of every page were selectable -- the cursor sat on them, and pressing A picked them -- while nothing was drawn there. A group's last words could be chosen only blind.

Eight is the cart's own page: easy_chat_2.c scrolls selectWordRowsAbove by four, and easy_chat_3.c's PrintECRowsWin2 prints row * 16 + 96 for each row. The loop now draws four, starting 2 px higher so the fourth row's 14 px of glyphs stop exactly at the bottom of the 78 px frame.

tests/engine/game3_easy_chat_page_test.lua drives the screen with a group larger than one page and checks all eight words reach the font, inside the frame. It reports the two missing words before this change.
2026-09-20 21:22:28 +02:00
bryanthaboi 66f117b064 Merge pull request #2355 from thibautbus/fix/gen2-contest-coins-strings 2026-09-20 13:33:17 -04:00
bryanthaboi 21947bd652 Merge pull request #2359 from 1Jamie/grandpas-garage 2026-09-20 13:32:30 -04:00
bryanthaboi 605654a086 CLOSES #2310 2026-09-20 13:21:16 -04:00
1jamie fc08349576 Fixes #2361 where:
1. The Cerulean City Policeman remained in front of the burgled house at `(30, 12)` even after receiving the S.S. Ticket from Bill.
2. Bill remained standing inside the cell separator teleporter door at `(3, 3)` upon returning to Sea Cottage after helping him.
2026-09-20 12:06:39 -05:00
1jamie 2d599700e3 fix(game3/battle): send out first usable pokemon when lead is fainted (closes #2358)
- State.new and Battle.start now default the initial active battler slot to the first usable (non-egg, HP > 0) Pokémon in the party instead of unconditionally selecting slot 1.
- In double battles, slots 0 and 2 correctly select the first and second conscious Pokémon.
- Add regression test in tests/game3_battle_fainted_lead_test.lua verifying single, double, and party-order behavior when the lead slot is fainted.
2026-09-20 11:50:33 -05:00
1jamie 4024b023f5 move _lastSentPal and _lastOpaque0 variables to file scope 2026-09-20 11:45:07 -05:00
1jamie c125abfcea fix(game3): fix viridian gym sliding door, npc jump movement, cachefs extraction, and battle performance
- Doors:
  - Fix Viridian City Gym and Dojo warp doors registering as wooden house doors; map metatile 0x15B to SlidingDouble animation and SE_SLIDING_DOOR (18).
  - Embed full FRLG metatile fallback lookup table and support CacheFs/Dataset.cache() for door textures and manifests.
- Scripting & Movement (Fixes #2356):
  - Add support for jump movement opcodes (jump_2_down, jump_in_place, etc.) for both player and NPC objects.
  - Implement scriptJump routines updating cell coords and triggering jump trajectory animations.
- CacheFS & GBA ROM Extraction:
  - Wire CacheFS persistence across storage, region map, battle AI/moves, items, pokedex chrome, multichoice, and easy chat extractors for Android portability.
  - Fix box storage chrome extraction palette/shadow colors and duplicate label bug.
  - Add region map chrome extraction and interactive Town Map rendering.
- Performance & Android Optimization:
  - Eliminate dynamic per-frame require/pcall lookups in battle render/update loops.
  - Pre-allocate particle sort comparator in AnimVm to avoid per-frame closures.
  - Cache palette shader uniform uploads and blend state in AnimPal/AnimVm.
  - Pool scratch color tables in FrlgFont to remove garbage generation during text token scanning.
  - Harden audio FFI caching and m4a worker initialization for sandboxed environments.
- Tests:
  - Add test suites for Viridian Gym doors, jump opcodes, storage chrome, and town map caching.
fixes #2356
fixes #2357
2026-09-20 11:41:16 -05:00
bryanthaboi bb098734ec Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	src/core/game3/battle/effects/secondary.lua
#	src/core/game3/battle/init.lua
#	src/core/game3/battle/ui.lua
#	src/ui/game3/bag_menu.lua
#	src/ui/game3/berry_pouch.lua
#	src/ui/game3/party_menu.lua
#	src/ui/game3/pokedex.lua
#	src/ui/game3/shop_menu.lua
2026-09-20 11:25:03 -04:00
bryanthaboi 0d5cf4f9e1 CLOSES #2303, CLOSES #2306, CLOSES #2307, CLOSES #2309, CLOSES #2312, CLOSES #2317 2026-09-20 11:22:57 -04:00
thibautbus 58624c3013 Route the Bug Contest status box through Strings()
The box above the menu during a Bug-Catching Contest (pokecrystal ContestStatus) drew its three labels and the empty-slot placeholder as bare literals: CAUGHT, LEVEL, BALLS: and "None". They were the last raw player-facing sinks in this file, which tests/gen2_ui_translation_harvest_test.py already lists among the modules that should have none.

The values keep the cart's own columns (x=8, 7, 8) only while the label fits them: "ATTRAPÉ" fills the seven tiles CAUGHT leaves, and "GEFANGEN" or "ATRAPADO" one more, so each value starts after its label instead of being drawn over by it.

"None" carries a context: the bare key is also the import wizard's "no limit" label (src/import/online/Wizard.lua), and one catalog entry cannot serve both -- which is also why the harvest gate asks for the three labels and not for it.

The caught mon's own name is left alone: it is a nickname, or a species the `pokemon` registry renames.
2026-09-20 14:34:38 +02:00
thibautbus 06d99eb2aa Route the two-row confirmation prompts through Strings()
The Bug-Catching Contest confirmation was drawn as two bare literals, one per row, with no hook a mod could reach. It becomes one catalog key, "Would you like to\nend the Contest?", the way every other confirmation prompt in this port is written (src/ui/StartMenu.lua, src/ui/gen2/SaveMenu.lua): "Would you like to" alone has no stable translation out of context, and the cart writes it as one text (data/text/common_2.asm).

Both prompts in this file now share one helper. It draws with Chrome.printWrapped, which wraps on the translated line break AND on pixel width, and reads the line count back from it to warn -- once per prompt -- when a translation needs a third row the box cannot hold. The title-screen prompt went through a hand-rolled splitter on the first line break, which handed Font.draw an embedded break, rendered as a space: a translation needing three rows printed two of them joined and ran past the box.

The official wordings fit: "Voulez-vous arrê- / ter le concours?" and "Möchtest du den / Wettkampf beenden?".

tests/engine/gen2_contest_coins_translation_test.lua drives StartMenu's confirmContest arm and StartGameCornerGame's no-coins branch with a mod-loaded catalog and checks the translated wording reaches the screen, plus the English case. On dev the three prompt checks fail; the refusal's own check passes there too, because that lookup already ran at runtime -- what dev was missing is the key, not the call.
2026-09-20 14:34:19 +02:00
thibautbus 53c320ed77 Route the Game Corner no-coins refusal through Strings()
StartGameCornerGame prints its refusal through Strings(NO_COINS_TEXT) already, but the literal sat behind a plain local, and catalog generation harvests the literals it can see at a Strings(...) / Strings.source(...) call site (tools/modkit.py's STRINGS_CALL). A literal hidden behind a variable is not one, so the key never landed in a generated catalog and the lookup had nothing to find.

Its neighbour one line below, NO_COIN_CASE_TEXT, already used Strings.source and did reach the catalog; this was the odd one out. Both are now in the harvest gate's REQUIRED set, which reports them as unharvested before this change.
2026-09-20 14:33:16 +02:00
bryanthaboi 547b3afc7d Merge pull request #2346 from thibautbus/fix/game3-translatable-strings 2026-09-20 08:12:47 -04:00
bryanthaboi 34f17b501a Merge pull request #2353 from thibautbus/fix/battle-flow-not-from-english-text 2026-09-20 08:12:31 -04:00
bryanthaboi b478c640c0 one one baby 2026-09-20 08:11:52 -04:00
thibautbus 29bbdfce43 Harden statusBlockedId, document the failed flag, cover the volatile clear
Three follow-ups on the commit below.

statusBlockedId read the status record's optional `id` field. A mod that overrides a status rather than patching it replaces the record outright, and `id` is optional in the registry (src/mods/Schemas.lua), so the field can be nil there and full paralysis would quietly stop clearing bide, thrash, charge and trapping. It now falls back on the key the record was looked up under.

`msgs.failed` is the only thing that suppresses a primary effect's success animation now, so it is no longer an internal detail: the move_effects registry documents it, and docs/modding/reference/registries.md is regenerated. That regeneration also picks up rows that had drifted from the schema before this branch (missText, trueColor, the sprite cell fields).

The parity suite gains the case the commit below changes the rules in -- PAR's own roll clearing the volatiles, and a flinch on a paralysed battler leaving them alone -- and its translated-label block now restores the shared Data.text entries through pcall, so a failing check there cannot leak "pioupiou zzz" into every later parity suite.
2026-09-20 12:03:54 +02:00
thibautbus 9bcc78e7f4 Translate the Easy Chat screen's own prompts and buttons
The Easy Chat editor (profile, questionnaire, battle-start message) drew all of its own text as literals: the screen's title and its two instruction lines, the "is as shown. Okay?" confirmation, the quit and delete-all confirmations, YES/NO, "Select a group." and the DEL. ALL / CANCEL / OK footer.

The per-screen strings are chosen when the screen opens, so they go through Strings() there. FOOTER_BTNS is a module-level table built before any catalog exists, so its labels are Strings.source and the draw loop looks each one up, the same shape as ItemsData.POCKET_LABEL; tests/game3_strings_module_tables_test.lua covers it.

The words the player picks are not touched: they come from the ROM's own Easy Chat word groups.

The title is still centred the way the cart centres it, but no longer starts left of the ribbon: the longest English title fills 118 of the header's 128 px, so a longer translation would have spilled out of the blue fill.
2026-09-20 11:55:05 +02:00
thibautbus aed01e63c9 Stop deriving battle-flow decisions from English message text
Three related call sites in the RBY/shared battle pipeline decided
in-game behavior by string-matching a message that had already been
routed through Strings()/romText -- correct in English, silently wrong
under any translation catalog, since a translated message no longer
contains the English substring being searched for:

- BattleState:sayStatusMsg played the sleep/confusion SFX by searching
  the status message for "is fast asleep!"/"is confused!". Under a
  translation catalog the SFX silently stopped firing.
  Status.beforeMove now returns onomatopoeiaKind/onomatopoeiaIndex,
  pinning the SFX to the specific still-asleep/still-confused message
  it belongs to (not necessarily the last one: confusion not
  self-hitting can fall through into the disabled-move check or the
  paralysis roll, appending one more, unrelated message after it).

- primaryEffectFailed decided whether to suppress a status/stat move's
  success animation by string-matching msgs[1] against six known
  English fail lines ("But, it failed!", "Nothing happened!", "didn't
  affect", "is unaffected", "protected by MIST", "already asleep").
  Extended the msgs.failed flag Substitute's own two failure lines
  already used (#644) to every other MoveEffects.primary handler that
  can fail without landing: changeStage, confuse, statusMove, and the
  single-condition guards in LEECH_SEED, HEAL (including REST),
  LIGHT_SCREEN, REFLECT, MIST, FOCUS_ENERGY, CONVERSION and DISABLE --
  every function actually reachable through primaryEffectFailed (only
  MoveEffects.primary entries are, via performMove's power == 0 and
  record.kind == "primary" gate; the many other "But, it failed!"
  sites elsewhere in this file belong to secondary/full-kind handlers
  on a different pipeline and don't reach this function at all, left
  alone here as a much larger separate follow-up). Removed the
  six-substring sniffer now that every real producer sets the flag
  explicitly; "is unaffected" had no live producer reaching this
  function to begin with (SWITCH_AND_TELEPORT_EFFECT's own copy goes
  through battle:cancelMoveAnim() directly, a different code path).
  Removing the sniffer also fixes three pre-existing test failures
  (parity_substitute_anim, two assertions in parity_trainer_ball_block)
  that were crashing on a TextBox.strip call this test environment
  doesn't stub -- that call only existed to support the sniffer.

- statusInterrupt decided whether a blocked move came from PAR's own
  63/256 full-paralysis roll (which clears bide/thrash/charge/
  trapping) by checking mon.status == "PAR" together with the message
  containing "fully paralyzed". mon.status == "PAR" alone isn't a safe
  replacement either: a paralyzed-and-flinched battler also has
  status == "PAR" while canMove == false comes from the flinch, not
  the paralysis roll. Status.beforeMove now also returns
  statusBlockedId, set to the status record's own id only when that
  record's own beforeMove handler is what returned canMove == false.

Extended tests/parity_status_onomatopoeia.lua with regression cases
for confusion overriding both ROM text labels with non-English text,
confusion stacked with a disabled selected move, and confusion stacked
with paralysis. Verified against the existing paralysis cases in
tests/mod_battle_tests.lua and the full test suite, no regressions.
2026-09-20 10:38:13 +02:00
bryanthaboi 7236afc11d Merge pull request #2351 from bryanthaboi/nexhas28-fix-yellow-badge-jingle-2339 2026-09-19 19:41:27 -04:00
bryanthaboi 96cb788cdf Merge pull request #2349 from Nexhas28/nexhas28-gen3-start-menu-coverage 2026-09-19 19:40:50 -04:00
Shane McGovern f11b9c762f fix(audio): play the badge jingle the cartridge plays (#2339)
Badge lines are end-battle texts (SaveEndBattleTextPointers), so they are
printed while the battle sound engine is still loaded.  A text sound command
names an id rather than a sound, and that engine's SFX_Headers_2 gives those
ids different sounds: the id behind sound_get_item_1 / sound_level_up is
SFX_Level_Up there, and the id behind sound_get_key_item is SFX_Ball_Poof.
The port resolved the jingle by name, so it always played the overworld
reading (Get_Item1 / Get_Key_Item) -- the wrong jingle for receiving the
BOULDERBADGE, and the same collision for Cerulean, Saffron, Cinnabar and
Viridian in Red, Blue and Yellow alike.

Add victories.badgeSoundFor(), which answers with the jingle that is actually
audible for the badge hand-over:

  Red/Blue  Brock Level_Up, Misty Ball_Poof, Sabrina Ball_Poof,
            Blaine Ball_Poof, Giovanni Level_Up
  Yellow    the same, except Misty: CeruleanGym.asm drops the command from
            that line, so Yellow's Cascade Badge has no jingle

Vermilion, Celadon and Fuchsia carry no sound on their badge lines in any
version.  The raw `badgeSound` field keeps the overworld-engine name, which is
what the TM texts (printed in the overworld) genuinely need.

Tests: pin the literal per-version jingle and its page for all five badge
leaders in tests/engine/gym_badge_jingle_page_bug1982.lua (53 checks), and
have the #1606 and driver suites assert through badgeSoundFor.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-19 23:28:31 +01:00
Shane McGovern 8fce7bcdc6 fix(gen3): ui.start_menu.items is raised on FireRed
Gen3Compat's src.ui.StartMenu coverage row claimed the
ui.start_menu.items hook is "not raised on FireRed yet". It is raised:
src/ui/game3/start_menu.lua calls it from StartMenu.show with
(game, entries), the same name and arity as src/ui/StartMenu.lua and
src/ui/gen2/StartMenu.lua.

The note is what a mod author reads when asking whether the hook exists
on this generation, so a stale one costs a mod the hook for no reason.

- Rewrite the note to describe the FireRed arm: a row is { id, label }
  and carries no onSelect to rewire, and a non-table return is dropped
  without the Logger.error the other two generations emit.
- List ENTRIES as backed and note it: the facade passes the module's
  list straight through, and show() rebuilds it on every open, so a mod
  has to act per open rather than once.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-19 23:08:57 +01:00
Shane McGovern 93e071bcaa Fix self-update on the PortMaster SBC build
The SBC and RG34XXSP ports hand LÖVE a source *directory*
(`love <dir>`), so `love.filesystem.isFused()` is false there and
`Boot.run` bailed on its first line.  The launcher still offered
"Update vX.Y.Z" and "Restart to update", so an update downloaded,
verified, and then was silently ignored forever.

- Boot.canUpdateInPlace() replaces the fused-only gate: a packaged
  build updates whether it is fused (AppImage, Flatpak game.love) or
  unpacked, and only a dev / source checkout (engine "0.0.0-dev") is
  excluded.  Fails closed when the host cannot be established.
  Prelaunch.updateAllowed now delegates to it, so the boot gate and the
  `--update` gate cannot disagree.
- Check.hostPort() reads the release-target marker, with
  POKEPORT_HANDHELD as a legacy fallback (the SBC launcher has always
  exported it, so packs predating this change still identify
  themselves).  The marker was read from the environment but never
  exported, so a full-package fallback on a handheld resolved to a
  desktop AppImage that cannot run there.
- A handheld now fetches its own package ("Download port update",
  reusing the worker's cross-platform download_full) instead of
  offering a URL it has no browser to open, and reports "Update package
  ready" once it is in the save directory for a manual re-extract.
- Launchers export POKEPORT_PORTMASTER / POKEPORT_RG34XXSP.

An in-place update only ever mounts a payload over the running source,
so relaxing the gate is less invasive than the fused path it joins.
A runtime bump still needs a full package, which the minShell gate
already reports as needs_full.

Tests: tests/engine/update_boot_host_gate.lua (new, 10 checks) pins the
gate; update_check_tests.lua pins both port asset names and hostPort().
scripts/test.sh --quick: all tiers passed; scripts/lint.sh: 0 errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-19 22:15:02 +01:00
thibautbus 180eda1b4b Keep the FireRed save window's values clear of translated labels
The save window draws its stat values at a fixed x, 56 px into the window, which pret places for the English labels. The translated labels are wider in several languages ("DUREE JEU", "SPIELZEIT", "TIEMPO J."), and the time then prints over its label.

The value column now starts past the widest label, with the English gap between them. English labels leave it where pret has it.
2026-09-19 23:05:01 +02:00
thibautbus d7f5895e84 Translate FireRed place names in the map popup and forest preview
The popup shown on entering an area takes its text from MapSectionsExtract.SECTIONS, the cart's English section names, with the floor suffix map_name_popup.c adds (" 3F", " B1F", " ROOFTOP") glued on in English. Nothing in a translation mod could reach either.

getInfo now also returns the name before the suffix (baseName), and the popup passes it through Strings() and translates the floor as a whole label ("3F", "B1F", "ROOFTOP"): the European carts number floors differently (3F is "2E" in French, "2S" in German), so a template around the number could not say it. The forest preview's name window (MapPreviewScreen), which shows the ROM's English section name, and the summary's met-location fallback go through Strings() too.
2026-09-19 23:05:01 +02:00
thibautbus 357e84ead5 Translate FireRed ability names where they are shown
Species, move, item and trainer names reach a translation mod through registries, but nothing renames abilities: battle messages ("<POKéMON>'s <ability> prevents ...") and the summary page printed the ROM's English name whatever the language.

Abilities.name and the summary's ability line now pass the name through Strings(), keyed by the cart's English name ("SPEED BOOST", "COMPOUNDEYES"). The identifiers the battle code compares (SPEED_BOOST) and the description lookup are unchanged.
2026-09-19 23:04:31 +02:00
thibautbus 7f4e54f312 Find FireRed summary descriptions by the ROM's move and ability names
SummaryData looks a move or ability description up as MOVE_<name> / ABILITY_<name>, built from the name the summary shows. A translation mod renames moves in place (the moves registry writes Pokemon._moveNames), so with one enabled the key became MOVE_<translated name>, nothing matched, and the summary and TM Case showed "---" instead of the description.

Pokemon.install now keeps a copy of the ROM's English move and ability names before any mod patches them, exposed as Pokemon.romMoveName / romAbilityName, and the description lookups use it, falling back to the name passed in when the pack has none.
2026-09-19 23:04:31 +02:00
thibautbus ed0fd91dbb Let a Strings() translation number its format directives
Strings() formats with string.format, which fills directives in argument order. A language that puts two values the other way round from English ("ATTACK de PIKACHU" for "PIKACHU's ATTACK") could not say so: the translation either kept the English order or swapped the values. The official FireRed, Gold and Red localizations reorder values like this in dozens of battle and menu lines.

A translation can now number its directives the way POSIX printf does ("%2$s de\n%1$s monte!"), and Strings() hands the arguments over in that order. Flags and widths are kept ("%3$03d") and "%%" stays a literal percent. A translation numbers all of its directives or none, and may only number arguments the source has; it may leave one out, as the official translations sometimes drop a name. Otherwise it falls back to the English source and logs one warning, like a translation with the wrong number of directives.

Sources never carry numbered directives, and a translation without them takes the same string.format path as before, so English and existing catalogs render the same text. A translation that still cannot format its arguments falls back to the English source formatted with them, instead of the raw template. An older engine given a numbered translation cannot format it and prints the template, so a catalog using numbered directives needs this engine.
2026-09-19 23:04:31 +02:00
thibautbus d74a96f645 Route FireRed intro, Pokédex, map and option text through Strings()
The new-game intro (controls guide, Pikachu pages, Oak's speech, name choices), the naming screen titles, the title menu, Pokédex headers and labels, the Town Map names and dungeon descriptions, option values and hints, the Help System bar, the Hall of Fame and the Trainer Card were plain literals.

The intro text tables stay English sources and are passed to Strings() when shown. Option values go through Strings() with the option key as context ("option.battleStyle"), so a mod can tell the SHIFT battle style apart from other uses of the word.
2026-09-19 23:04:31 +02:00
thibautbus 85b6b96d61 Route FireRed menu text through Strings()
The bag, TM case, Berry Pouch, party, PC, box storage, shop, summary, save and yes/no menus drew their labels, prompts and result messages as literals, so a translation mod could not reach them.

Messages built at call time go through Strings() with whole-sentence templates. Action lists the menus compare against ("CANCEL", "EXIT", "SWITCH BOX"…) keep their English identifiers and are translated where they are drawn, as are the module-level PC labels, descriptions and TEXT_* strings, now marked with Strings.source(). Default box names are stored in English ("BOX 3") and shown translated, while names the player typed are drawn as they are.
2026-09-19 23:04:31 +02:00
thibautbus 17f6e68207 Route FireRed field, item and script messages through Strings()
Field move texts, item-use results, Pokédex and summary fallbacks, trainer class and send-out lines, step events (repel, whiteout, fainting), VS Seeker and multichoice labels were plain literals or string.format calls, so a translation mod could not reach them.

Messages built at call time now go through Strings() with whole-sentence templates. Tables built at module load keep English sources marked with Strings.source() and are translated when read: FieldMoves.TEXT through an __index proxy, the Oak's lab rival fallback dialogs when the trainer is built, and ItemsData.POCKET_LABEL by its caller. Nature names and ability/move descriptions are translated where the summary asks for them. Values the engine stores or compares (box names, identifiers, quest-log keys) are left as they are.
2026-09-19 23:04:31 +02:00
thibautbus b97c683743 Route FireRed battle messages through Strings()
Every message the game3 battle engine prints was built by concatenating English fragments around names ("X" .. " is hurt\nby poison!"), so a translation mod could not reach them, nor phrase a sentence other than as English splits it. They are now whole-sentence Strings() templates with the names as arguments (Strings("%s is hurt\nby poison!", name)), which renders the same English and lets a catalog word the whole sentence around each value. The one visible change is that effect messages now name abilities through Abilities.name, as the cart prints them (COMPOUNDEYES, LIGHTNINGROD) rather than from the identifier (COMPOUND EYES). Values still fill the template in call order (Strings() formats with string.format).

Fragment tables become full templates: status infliction, charge-turn and semi-invulnerable moves, Truant, berry flavours, held-item and enemy-item cures, and the stat-change lines (one template per rose/sharply rose/fell/harshly fell instead of a translated verb glued after the stat). Tables built at load time mark their keys with Strings.source() and are translated where they are said, since no catalog exists yet when the module loads. Stat names, the Ally/Foe prefixes, weather lines and screen names are translated at the time they are shown; the "a boosted" EXP line and the trainer send-out line become one template per variant rather than a fragment passed as an argument.

Code that read the English text back is fixed too: the double-battle send-out (animated and headless intros) no longer parses the trainer out of the translated "sent out" line, the battle UI paces the move-used line by its catalog wording, and AnimSeq recognises the miss, confusion, substitute, move-used and fainted lines it paces animations on through their catalog wording.
2026-09-19 23:04:31 +02:00
thibautbus 56504a38ec Load the merged Strings catalog on FireRed boots
Game and Game2 hand the merged mod data to Strings.load right after the mod merge, so a translation mod's strings registry is what Strings() answers from. Game3:_loadMods did not: on a FireRed boot Strings() kept the catalog the launcher preloaded, which is every enabled mod's lang/strings.lua whatever game it targets, with no defined precedence, and never a registration made from mod code. Call Strings.load(self.data) after the merge, as the other two games do.
2026-09-19 23:04:18 +02:00
bryanthaboi 2359287036 Merge pull request #2342 from thibautbus/fix/frlg-latin-glyphs 2026-09-19 16:44:44 -04:00
1jamie 0ec205db1e fix: resolve PC animation state bugs and update map event bit extraction logic 2026-09-19 15:23:43 -05:00