Skip per-frame mouse warps and FlexLove perf sampling on Switch, feed pad coords through a getPosition bridge, and park that shim before the save editor so desktop paths stay unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
A shared imports/ inbox with Red+Yellow was starting Red from the Yellow tab; match by GameVersion.forSha1 for the selected game and document the tab-scoped rescan.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring FlexLove launcher UI ("big ui moment"), iOS picker dismiss, and Metal updates into the Switch NX branch. Keep pack_love (with libs/), NX inbox/rescan paths, SwitchDiagnostics, and Save Editor PadInput; port Scan again labels into LauncherView.
Co-authored-by: Cursor <cursoragent@cursor.com>
scanInbox never consulted ready, and mobileFileBridge already mirrors
android on Android/iOS, so the dual guard was a no-op.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ship stock engine chords only; community mods own their rebinds, and keys
2/3/5 are claimed by the engine before pipeline hotkeys run.
Co-authored-by: Cursor <cursoragent@cursor.com>
Write the imported-sha1 newline with string.char(10) so gate_strings_coverage
does not treat the filesystem ledger separator as player-visible text.
Co-authored-by: Cursor <cursoragent@cursor.com>
Split Import/Export paths into imports/saves/{red,blue,yellow}/ and
exports/{red,blue,yellow}/ so MTP destinations match each launcher tab.
Co-authored-by: Cursor <cursoragent@cursor.com>
Retire successful imports to *.sav.imported and record content hashes so
re-pressing Import save (or the same bytes under a new name) cannot clone
slots. Surface multi-import counts and the active game tab in the notice.
Co-authored-by: Cursor <cursoragent@cursor.com>
pokered plays no 'pitched-down faint cry': the player mon's faint is its
ordinary species cry (RemoveFaintedPlayerMon -> PlayCry) with no
Faint_Fall, and the enemy faint plays no species cry at all -- trainer
battles get SFX_FAINT_FALL then SFX_FAINT_THUD, wild battles go straight
to the victory music (FaintEnemyPokemon core.asm:732-796).
The port played the species cry AND Faint_Fall on every faint, so a
fainted enemy sounded its full battle cry and a fainted player mon got
the fall whistle the hardware never plays.
BattleState.onFaint now:
- player: Sound.playCry only
- enemy trainer: Faint_Fall then Faint_Thud (after the slide)
- enemy wild: no faint sfx (victory music already queued)
Adds tests/parity_faint_cry_bug709.lua asserting the per-side sequence.
Fixes#709
Bring feat/switch-nx up to date with origin/dev (72 commits). Resolve
Input/RomImporter conflicts by keeping GamepadMap (NX face remap + dual-path
gate) while adopting upstream joyBindings rebinds (#632) and Enable-all mods
(#647). Gate shoulder GAME SPEED hotkeys when Select is held so Select+L
display chords still work.
Co-authored-by: Cursor <cursoragent@cursor.com>
Issue #665: the Windows ROM and save pickers returned the raw chosen
path. io.open on Windows needs ANSI bytes, so a path with accented
characters (Pokemon -> Pok\x82mon, or a folder like 'Pokemon Gen1')
could never be opened -- the same bug #325 already fixed for mod zips
by copying to a plain-ASCII temp name.
Apply that fix to the ROM and .sav pickers: each copies its pick to an
ASCII temp name (pokeport_rom_pick.gb / pokeport_sav_pick.sav) before
answering, exactly like chooseZip does.
Also sanitize the prompt strings interpolated into the picker shell
commands: '%' would be eaten as a string.format directive, and quotes
would break the AppleScript/zenity argument or the surrounding shell
string.
Fixes#665
When a Pokemon faints in battle, onFaint() clears it from the battle
participant set. The revive item restored HP but never re-added the
mon to self.participants, so at awardExp() time the revived mon passed
the HP check but failed the participant gate -- getting no exp.
Fix: re-add the revived mon to battle.participants in the revive item
effect, so it's counted as a participant and receives its share of
experience at battle end.
Fixes#648
The SummaryMenu (pause menu stats screen and in-battle stats screen)
displayed the base PP from the move definition as the max value,
ignoring PP Up bonuses. After using a PP UP, the screen would show
e.g. 6/5 instead of 6/6.
Fix: calculate maxPP with the PP Up bonus (basePP + ppUps * basePP/5),
matching the formula used everywhere else -- battle fight menus,
ETHER restore, Pokemon Center heal, link protocol, and save editor.
Fixes#641
Add 3X as a speed option between 2X and 4X in GameSpeed.LEVELS (#677).
Add controller hotkeys: rightshoulder (R2) cycles speed up through the
level list, leftshoulder (L2) cycles speed down. Keyboard equivalent
is hotkey 1 (cycles up). All hotkeys are gated during transitions,
scripted cutscenes, and link play (same guards as the color hotkey
at key 2).
The _cycleSpeed helper wraps the save-options update with the same
busy/overworld guard used by the existing color-cycle hotkey.
Fixes#677
Issue #695: Pressing B in PC submenus (BoxMenu, PlayerPC) was exiting
the entire PC session instead of returning to the main PC menu. The
three main-menu items (Bill's PC, player's PC, Prof. Oak's PC) were
missing keepOpen=true, so selecting one popped the main menu off the
stack. Added keepOpen to all three, matching the pattern already used
by BoxMenu and PlayerPC's own rows.
Issue #694: Falling through boulder holes in Seafoam Islands, Victory
Road, and Pokemon Mansion played no sound effect. Added Faint_Fall sfx
before every hole warp -- the scripted onStep holes in seafoam.lua,
story.lua, and story6.lua, plus the warp-tile-based hole detection in
OverworldController takeWarp. Faint_Fall is the companion to Faint_Thud
(already played when boulders fall into holes).
Fixes#695Fixes#694
The Celadon Diner uses three LOBBY table blocks that share tile 0x37 on
their flat surfaces. Only block 29 had the 0x37->0x5a BROWN alias; blocks
45 and 49 showed raw tile 0x37 in ROOF (blue-gray), creating a blue
square on the second/third tables with the Advanced Colors preset.
Add alias entries for blocks 45 (cells 13/14) and 49 (cells 1/2).
Fixes#689
apply() returned false on its first line for mobile, so the option did
nothing there. A phone has no window to resize, so the lock caps the
render scale instead: the largest whole multiple of 160x144 the display
holds, centred, black around it.
Two parts beyond that. The scale is read off the display rather than from
the desktop's 1X-4X ladder, which named a different fraction of every
device and left the useful levels off the list; mobile shows ON or OFF.
And the world pass, which expands to cover the whole display so letterbox
becomes more map, is now sized against the locked viewport, so the lock
reaches the overworld instead of showing more of it.
Pixel perfect throughout, whole multiples only. Desktop and OFF are
unchanged. Renames the row to FAITHFUL RATIO on both platforms; the saved
key stays faithfulRes so existing settings carry over.
Edge docking and zoom-linked UI scaling shipped as unconditional
behaviour. Both are departures from how the port composed the screen, so
they become a setting instead: UI LAYOUT = CENTERED (the default) or
DYNAMIC.
CENTERED is a fixed letterbox. Elements stay where they were drawn in the
160x144 canvas, and the UI does not follow the survey zoom, so screen
furniture neither moves nor resizes under the player. That is what the
pre-anchoring builds did. DYNAMIC is the current behaviour, unchanged.
Both halves matter together: gating only the anchoring would stop the
dialogue box moving but leave it resizing with the zoom, which is the same
complaint in a different form.
Gated at Renderer:setUIAnchor and Renderer:uiScale rather than at each
caller, so one switch covers the dialogue box, its YES/NO, the START menu
and anything anchored later, and no caller knows the option exists.
Game.dynamicUI answers true only for an explicit "dynamic", so a save
written before this keeps the layout it already had.
Independent of it, deliberately: BATTLE SIZE still works under either mode
(uiFill overrides the scale later, in endFrame), and a battle still holds
its own prompts inside its screen under DYNAMIC.
Also includes the Oak intro fix (previously #674): the speech fills white
over the UI canvas while its dialogue box docks to the window edge, so
under DYNAMIC black showed between the two. letterboxWhite closes it, and
the shrink beat's replica box rides the same anchor as the real box it
stands in for.
The faint slide was shortened from 30 to Timing.FAINT_SLIDE (14) frames
in the timing-parity pass, but fxFaintOffset still computed the offset
with a stale (30 - frames) * 2. With frames starting at 14 the sprite
teleported 32px down on the first frame and only slid the remaining
28px, cutting the animation short.
SlideDownFaintedMonPic drops the pic one 8px row per 2-frame step, so
the offset advances Timing.FAINT_SLIDE_STEP (4px) per frame at 1x and
covers the full 56px PIC_HEIGHT over the 14-frame budget.
The trainer intro was the only enemy send-out path that never set
enemySendingOut, so the front sprite drew at full size the moment the
trainer pic walked off, held through "X sent out Y!", and the grow-in
then played over a mon that had already arrived. Set it with the pic
teardown and clear it with startGrowIn, matching the mid-battle
replacement and the player's own send-out.
Also repairs two parity suites that could not load at all: their game
stubs lacked input.isDown, which battle text has read every frame since
typing started honouring PrintLetterDelay. intro_chrome additionally
pressed A inside PromptText's ProtectedDelay3 hold, which ignores the
button for TEXT_PRE_ADVANCE frames.
BATTLE SIZE "fixed" draws the battle as a discrete letterbox rather than
filling the window, and BATTLE BG "world" composes it over the live map.
Everything the battle then opens broke out of that composition, because
each piece of the frame's geometry was read off a fact about THIS FRAME
instead of about the battle:
* Renderer:uiScale follows the survey zoom only while a world is behind
the UI, gated on worldActive -- this frame's world pass. PartyMenu and
ListMenu are opaque, so pushing one makes StateStack:visibleBase skip
the map, no world pass runs, and the menu loses the step-down and blits
a whole integer scale larger than the battle it just covered. Held
with uiWorldHold, the same whole-stack rule uiFill and the battle dim
already use. ("fill" hid this: it overrides the scale outright.)
* Game:draw started the frame at visibleBase, so that same opaque menu
cut the overworld -- and the world pass with it -- out of the frame
entirely, collapsing a "world" backdrop to endFrame's flat black clear.
A world-bg battle now keeps the frame starting from underneath itself
(drawBaseInStack). Only the START of the draw moves; the clear stays
keyed to the real visibleBase, so the menu still gets its opaque canvas
and draws exactly as before.
* worldZones was keyed to that same clear base, so it came out nil for a
frame whose world pass HAD run -- dropping endFrame's world blit onto
the UI zone list instead, smearing the party menu's own HP-bar palettes
across a world-canvas-sized image. Keyed to whether the map drew.
* endFrame's letterbox clear read letterboxWhite off visibleBase alone,
so an opaque menu over a BG "white" battle flipped its surround to
black the same way. Same whole-stack hold.
* ChoiceBox bottom-anchored unconditionally, docking it to the WINDOW
edge. That is only right when it rides the dialogue box beneath it,
which is anchored there too; TextBox now passes the anchor and nothing
else does, so the battle's switch offer and the shop/PC confirms stay
over the screen that pushed them.
* TextBox anchors likewise: a battle is a self-contained SCREEN, not the
window, and pokered prints its text box in the same 160x144 tilemap as
the HUD. The caught-mon nickname prompt was landing a whole letterbox
below the blanked battle field it is printed on. BattleState.holdsUI-
Anchors holds setUIAnchor off while a battle is in the stack; the
overworld's own dialogue box still docks to the screen edge.
several places (Commands.ask, the give-a-nickname prompt, PC box release/
change confirms, start menu save/quit) pushed a bare ChoiceBox after the
preceding text box already popped on an A press, instead of riding
TextBox's opts.choice like the rest of the engine. the YES/NO box now
comes up while the question is still on screen, matching the original.