Introduce an OPTIONS -> PERFORMANCE setting that scales the port's
optional presentation extras down for weaker hardware, so older/lower-end
devices can run the game smoothly.
The tier governs the three heaviest non-faithful extras -- the 3D TILT,
the GBC FX post-process shader, and survey ZOOM (which renders connected
neighbor maps) -- plus a hard FPS ceiling. It never touches game logic,
which is fixed-step off dt, so every tier plays identically.
- src/core/Performance.lua: tiers (auto/high/balanced/low), a conservative
device auto-detect (ARM handhelds -> low, phones -> balanced, normal
desktops -> high), per-tier caps, and the option-row cycle. Zero
requires, like GameVersion.
- Game:applyOptions clamps the *live* presentation state against the tier
without rewriting stored options, so a lower tier hides the player's
TILT/GBC FX/ZOOM/FPS choices and a higher tier restores them exactly.
- Zoom.offsetRange floors the range at FIT when survey is disallowed, so
the option row, hotkey, and mouse wheel all stop at close-up on LOW.
- New save.options.performance default "auto"; OPTIONS row heads the
display group and re-applies live.
- Tests: tests/engine/performance_tiers.lua (ROM-free); mod_ui_tests row
golden updated for the spliced row.
AUTO resolves to HIGH on a normal desktop and on every options.lua that
predates the option, so the common case is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6bFAiQyZ5jDmewsbB4LG9
Follow-up to #452, from dogfooding the merged build on a real iPhone.
A mod fused into game.love sits in the read-only app bundle: the mod
manager's Delete can't remove it and it reappears every launch, and an
imported newer version can never fully replace it. iOS now packs no
mods, matching every other platform - mods install as .zips at runtime
with a real install/upgrade/delete lifecycle.
With nothing fusing it, mods/pokewalker moves out of this tree to its
canonical home, https://github.com/mresnick67/Gen1ReComp-Pokewalker
(already at 0.2.0 there with move learning and a paged credit dialog;
in-tree 0.1.0 was stale). docs/ios-install.md points at it. The
event:save.loaded coverage-debt entry returns to gate_meta_coverage:
its coverage came from the mod's test suite and leaves with it.
Local reproduction of the ci workflow on this branch: all tiers, mod
lint, fingerprint + meta-coverage gates green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Makes the iOS build a first-class citizen: ROM/mod/save import through
the system document picker (the README's missing "UIDocumentPicker
handoff"), Files-app drop-in support, and an opt-in Apple Health
step-sync seam consumed by a new gallery mod (Pokewalker).
Native layer (mobile/ios/native/, wired by mobile/ios/patch_love_src.py
on every build, so the fetched love-src tree stays pristine + re-patchable):
- GRPickerBridge.swift: love.system.pickFile("rom"|"mod"|"sav") and
love.system.createFile on iOS with the same contract as love-android's
SAF picker (picked_rom.gb / picked_mod.zip / picked_save.sav /
export_done.flag in the save dir). Reached from wrap_System.cpp via the
ObjC runtime, so liblove needs no Swift interop.
- GRBootstrap.m: sweeps .gb/.gbc/.zip/.sav dropped in Documents (Files
app / Finder sharing) into the save dir on every activation;
UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace in the plist
overlay. Drop a ROM, open the app, it imports with zero taps.
- GRHealthBridge.swift: love.system.syncHealthSteps() -> read-only
HealthKit step query anchored to the last sync, delivered as
steps_pending.json (merge-not-overwrite). HealthKit entitlement +
usage description included.
Lua:
- RomImporter: iOS rides the Android mobile flows; a 0.5s poll consumes
picker deliveries (iOS pickers are in-process modals, so the Android
refocus rescan never fires); failed pick copies surface as an
on-screen notice via pick_error.txt.
- main.lua: on iOS, stop forwarding touchpressed to the Importer - LOVE
already synthesizes a mousepressed for the primary touch, and the
resulting same-frame double-present made the document picker
auto-dismiss with zero documents (silent import failure).
- mods/pokewalker: opt-in Pokewalker mod (manifest v2, MECHANIC,
permissions declared, mod.card, CHANGELOG, headless test suite 9/9,
modkit validate --base imported + lint clean). Fused into iOS
game.love only; loads dormant anywhere without the bridge.
Build (scripts/build_ios.sh):
- Fix Xcode 26: the global PRODUCT_NAME override also renamed liblove.a
and broke the app link; the app bundle is renamed after the build
instead.
- Fix nondeterministic pack failures: grep -q + pipefail races SIGPIPE
on the game.love content checks.
- Simulator builds sign ad-hoc so entitlements embed (HealthKit works in
the simulator).
- Device builds: signing team auto-detected from the keychain,
CODE_SIGN_STYLE=Automatic + -allowProvisioningUpdates for CLI-only
provisioning, per-team derived bundle ID (explicit App IDs are
globally unique, so third parties can't sign the project default),
gitignored mobile/ios/bundle_id.local pin, and --install to push to a
connected iPhone.
- docs/ios-install.md: a zero-knowledge walkthrough from bare Mac to
playing on an iPhone.
Backward compatibility: no behavior change on desktop or Android. The
new love.system functions exist only under LOVE_IOS; RomImporter's
mobile flag simply includes iOS alongside Android; the main.lua change
is iOS-gated; the Pokewalker mod is packed only by the iOS build script
and its option defaults off.
Verified on an iPhone 17 Pro simulator and an iPhone 16 Pro device:
scripted ROM import to title screen, Files-drop zero-tap import,
picker-driven mod install and save import/export, HealthKit permission
sheet + step credit (4000 steps -> +200 EXP at the default rate through
the engine growth curve).
On a window too short for the stacked single-column layout -- a phone, or a
narrow desktop window -- the ROM / SAVE FILES / Play / SAVE SLOT stack ran
past the bottom of the window while the footer stayed pinned there and drew
over it. Nothing clipped the panel and nothing scrolled it, so the overflow
was unreachable.
Everything under the tab bar (panel, updater banner, footer) is now one
scrolling column, used only when it is taller than the room below the tab
bar. The strip, logo and tab bar stay pinned, so navigation is always on
screen, and the footer is laid out downward from footerTop right after the
content instead of upward from the window bottom.
- RomImporter.pageScrollFor is the whole decision, pure and covered by
tests/engine/launcher_page_scroll.lua. A window that grows back drags the
offset down with it, so the page never stays parked past its own end.
- The panels return their natural height as they draw, so the measurement is
the previous frame's: the same one-frame settle the slot and mod lists
already rely on.
- One scroll axis at a time. While the page scrolls, the panels draw paged:
the slot and mod lists take their natural height, keep no inner scroll
region and report a max of 0, so wheel, right stick and drag all move the
page. Two-column layouts do not overflow, paged stays false, and every one
of these behaves exactly as before.
- inside() and _ptIn() reject a rect that scrolled out of the viewport, so a
control that slid under the tab bar cannot be clicked through it. Tab chips
are pinned and exempt. pageScroll resets on a tab change.
Android had no scroll gesture at all: the launcher is handed no move events
(main.lua forwards neither touchmoved nor mousemoved while it is up) and its
mouse emulation was never trusted, which is what "no reliable pointer
polling" referred to. That was survivable while every scroll region was an
inner list, and useless once the page itself scrolls, since a phone is
exactly where it overflows. love.touch is pollable, so _pointerHold reads the
first active touch there and hands _updateSlotDrag the same (held, y) pair
the mouse gives on desktop. Slot rows and mod toggles consequently arm on
press and commit on release on Android too, matching desktop, so a swipe that
starts on a card scrolls instead of selecting it. All of it is gated on
touchPollable: without love.touch every Android path is exactly what it was.
conf.lua also grows minwidth/minheight (480x360) for the desktop window,
under which the cards stop being readable. Mobile is fullscreen and ignores
it.
Verified on the Android emulator (1080x2400) against a build of the parent
commit: before, the footer painted over the SAVE SLOT card with no way to
reach it; after, the page pans by touch and the footer is reachable and
intact.
The reporter labels runs by abusing the in-game player name; give slots
a real label instead. SaveData.renameSlot persists a trimmed label in
the options registry (options.saveSlots[version].names) -- never in the
save file, so renaming needs no save rewrite and an empty slot can be
labeled too -- listSlots rows carry it as `label`, and deleteSlot
drops it with the slot.
In the launcher, right-clicking a slot row opens an inline rename modal
(Enter commits, Esc cancels, empty clears; 24 whole-codepoint cap via
local UTF-8 helpers, since plain luajit has no utf8 library). The row
title shows the label over the player name; badges/time/caught stay on
the meta line. Desktop-only: touch has no secondary button. main.lua
now forwards love.textinput to the importer while it is up.
Backend covered by a new renameSlot block in tests/engine/save_slots.lua
(78/78); docs/launcher.md's registry section documents the label.
# Closed issues
CLOSES#17: Incorrect Character Visuals (Only with GBC filter)
CLOSES#23: Could you allow the player to change the order of the moves
CLOSES#24: Evolution music not playing during evolution
CLOSES#26: Standing on door glitch
CLOSES#27: Battle Intro text automatically continues
CLOSES#29: Visual bug when zoomed out
CLOSES#32: Team Rocket recruiter doesn't battle with you unless you speak with him first
CLOSES#33: Developer/Debug Console
CLOSES#35: Professor Oak's introduction Inaccuracies
CLOSES#36: Missing Pokemon Dex entries when picking starter + Rival Pathing issues
CLOSES#39: Guy who stops player from skipping brock doesn't bring you to brock's gym + doesn't leave once you've beaten brock
CLOSES#40: Bill cutscene is broken
CLOSES#41: Ticket guy failing to be a Ticket guy
CLOSES#42: S.S. anne odd behavior + Missing sailing away animation
CLOSES#43: Dig Attack animation appears to be glitched
CLOSES#44: Pokeball flashing doesn't appear to be accurate
CLOSES#45: Inaccurate Cut Animation
CLOSES#46: Dugtrio i caught in diglett cave has two of the same move
CLOSES#47: Rival ignores player in Lavender Tower
CLOSES#48: Healing pad in lavender tower does not function
CLOSES#49: Incorrect dialogue with parched security guard
CLOSES#50: (Game Breaking!) Rocket grunt guarding poster refuses to move
CLOSES#51: Badges showing up as items I can deposit in PC
CLOSES#52: Visual bug on Celadon Department Store roof (Red Filter)
CLOSES#54: Visual issue on route 15 + Fuchsia City
CLOSES#56: Running animation missing
CLOSES#57: Safari Zone does not display steps while you are inside of it
CLOSES#58: (Game Breaking!) Softlock at cycling road gate
CLOSES#59: Bike visual issues
CLOSES#60: Cycling road not forcing you to get on your bike
CLOSES#61: No keycard doors in Silph Co.
CLOSES#63: Missing teleporter animation
CLOSES#64: Inaccurate spinning
CLOSES#65: Reimplement unused Silph Co. Chief and Professor Oak trainer battles