Compare commits

...

7 Commits

Author SHA1 Message Date
bryanthaboi cecb24bb0e Merge pull request #480 from bryanthaboi/dev
johnny boys fixes
2026-07-30 13:55:19 -04:00
bryanthaboi b99b497893 Merge pull request #479 from johnjohto/fix-save-editor-items
Fix save editor item crash on Android
2026-07-30 13:53:24 -04:00
johnjohto e16a45d6ba Avoid invalid save editor clips
On compact Android viewports the item picker can end up with no list
space. Passing its negative height to setScissor crashes LÖVE.
Use an empty clip region instead.
2026-07-30 13:28:18 -04:00
bryanthaboi 0e29046c31 Merge pull request #458 from johnjohto/fix-yellow-bills-pikachu
Add Bill's House Pikachu scenes
2026-07-30 13:02:50 -04:00
johnjohto 5cbada493a Add Bill's House Pikachu scenes 2026-07-30 12:52:17 -04:00
bryanthaboi 4c79db3d5d Merge pull request #472 from bryanthaboi/dev
stupid ass android
2026-07-30 12:28:57 -04:00
bryanthaboi c7e46b6563 stupid ass android 2026-07-30 12:26:27 -04:00
9 changed files with 224 additions and 3 deletions
+8
View File
@@ -78,6 +78,14 @@ function love.conf(t)
-- player to copy their ROM there instead of needing a native file
-- picker (LOVE 11.5 on Android has none -- see src/import/RomImporter.lua).
t.externalstorage = osName == "Android"
-- LOVE 11.x exposes the phone's accelerometer as a 3-axis joystick by
-- default. Gravity keeps one axis pinned near +/-1.0 whenever the phone
-- is held upright, so it streams past-deadzone joystickaxis events that
-- both hid the touch overlay every instant (it reads as "a controller is
-- in use") and, via the generic-joystick axis mapping (#459), walked the
-- player around by tilt. Nothing in the game reads the accelerometer,
-- so drop the device entirely (#468).
t.accelerometerjoystick = false
else
t.window.resizable = true
end
+4
View File
@@ -183,6 +183,7 @@ M.BILLS_HOUSE = {
overworld = ow },
"BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON")
game.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
require("src.world.PikachuFollower").onBillEnteredMachine(game, ow)
done()
end
if ow.player.facing == "down" then
@@ -273,6 +274,9 @@ M.BILLS_HOUSE = {
Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1")
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL2")
end
if not flags.EVENT_MET_BILL_2 then
require("src.world.PikachuFollower").onBillsHouseEnter(game, ow)
end
end,
}
+15
View File
@@ -491,21 +491,36 @@ function Game:gamepadaxis(joystick, axis, value)
Input:gamepadaxis(joystick, axis, value)
end
-- conf.lua turns the mobile accelerometer-joystick off (#468), but guard the
-- generic joystick path anyway: any sensor-style device that still reaches us
-- has gravity pinning an axis past the deadzone, which would hide the touch
-- overlay every instant and steer the player by tilt through the axis-1/2
-- mapping (#459). Real controllers arrive as SDL gamepads or named sticks,
-- never as "* Accelerometer".
local function isAccelerometer(joystick)
local name = joystick and joystick.getName and joystick:getName()
return name ~= nil and name:lower():find("accelerometer", 1, true) ~= nil
end
function Game:joystickpressed(joystick, button)
if isAccelerometer(joystick) then return end
TouchControls:noteGamepad()
Input:joystickpressed(joystick, button)
end
function Game:joystickreleased(joystick, button)
if isAccelerometer(joystick) then return end
Input:joystickreleased(joystick, button)
end
function Game:joystickaxis(joystick, axis, value)
if isAccelerometer(joystick) then return end
if math.abs(value) > 0.5 then TouchControls:noteGamepad() end
Input:joystickaxis(joystick, axis, value)
end
function Game:joystickhat(joystick, hat, direction)
if isAccelerometer(joystick) then return end
if direction ~= "c" then TouchControls:noteGamepad() end
Input:joystickhat(joystick, hat, direction)
end
+1
View File
@@ -2093,6 +2093,7 @@ function OverworldState:billsHouseBillExits()
local Commands = require("src.script.Commands")
local ctx = { game = Game, save = Game.save, overworld = self }
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1")
require("src.world.PikachuFollower").onBillExitedMachine(Game, self)
local function done()
Game.save.flags.EVENT_MET_BILL = true
Game.save.flags.EVENT_MET_BILL_2 = true
+66
View File
@@ -189,6 +189,9 @@ function PikachuFollower.current(ow)
end
function PikachuFollower.onMapEntered(game, ow, opts)
-- Bill's House owns a short scripted scene that deliberately keeps
-- Pikachu off the normal trailing loop. A new map instance ends it.
ow.pikachuBillsScene = nil
remove(ow)
if not shouldSpawn(game, ow) then return end
-- opts.keepPikachu is the follower a connection crossing kept alive:
@@ -393,6 +396,7 @@ end
-- (pikachu_follow.asm keeps it one walk step behind)
function PikachuFollower.update(game, ow)
if ow.pikaHop then return end -- the counter hop owns the follower (#417)
if ow.pikachuBillsScene then return end
local npc = findFollower(ow)
if not npc then
if shouldSpawn(game, ow) then PikachuFollower.onMapEntered(game, ow) end
@@ -741,6 +745,68 @@ function PikachuFollower.picLift(emote)
return 0
end
-- Bill's House has three map-scripted Yellow companion beats
-- (BillsHouseScript0/2/5): Pikachu walks over to investigate Bill, waits at
-- the cell separator, then reacts when Bill reappears. Keep it at the
-- machine until this map instance is discarded, just like the cartridge's
-- disabled following state.
local function billsHouseEmotion(game, ow, npc, bubble)
local Sprites = require("src.pokemon.Sprites")
ow.emote = {
npc = npc, frames = 50, bubble = bubbleIndex(game, bubble) or false,
pikaPic = Sprites.path(game.data, "PIKACHU", "front",
{ kind = "overworld" }),
}
end
local function movePikachu(ow, npc, steps, onDone)
npc.goalX, npc.goalY = nil, nil
idleReset(npc)
local function nextStep(i)
local step = steps[i]
if not step then
if onDone then onDone() end
return
end
ow:scriptMove(npc, step[1], step[2], function() nextStep(i + 1) end)
end
nextStep(1)
end
function PikachuFollower.onBillsHouseEnter(game, ow)
if not (GameVersion.isYellow() and ow.map and ow.map.id == "BILLS_HOUSE") then
return
end
if game.save.flags.EVENT_MET_BILL_2 then return end
local npc = findFollower(ow)
if not npc then return end
ow.pikachuBillsScene = true
movePikachu(ow, npc, { { "right", 3 }, { "up", 1 } }, function()
billsHouseEmotion(game, ow, npc, "QUESTION_BUBBLE")
end)
end
function PikachuFollower.onBillEnteredMachine(game, ow)
if not (GameVersion.isYellow() and ow.pikachuBillsScene) then return end
local npc = findFollower(ow)
if not npc then return end
local steps = ow.player.facing == "down"
and { { "up", 3 } }
or { { "up", 1 }, { "left", 1 }, { "up", 2 }, { "right", 1 } }
movePikachu(ow, npc, steps, function()
billsHouseEmotion(game, ow, npc, "QUESTION_BUBBLE")
end)
end
function PikachuFollower.onBillExitedMachine(game, ow)
if not (GameVersion.isYellow() and ow.pikachuBillsScene) then return end
local npc = findFollower(ow)
if not npc then return end
idleReset(npc)
npc.facing = "left"
billsHouseEmotion(game, ow, npc, "EXCLAMATION_BUBBLE")
end
-- ---------------------------------------------------------------------
-- PikachuWalksToNurseJoy (engine/pikachu/pikachu_emotions.asm, run by
-- engine/events/pokecenter.asm once the heal is accepted): the companion
+89
View File
@@ -0,0 +1,89 @@
-- Yellow starts Bill's House with Pikachu's confused reaction. The map
-- script owns that one-shot, while PikachuFollower owns the movement.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("parity Yellow Bill's Pikachu")
local check = S.check
local entered = 0
local originalFollower = package.loaded["src.world.PikachuFollower"]
package.loaded["src.world.PikachuFollower"] = {
onBillsHouseEnter = function()
entered = entered + 1
end,
}
local story = dofile("data/scripts/story.lua")
local game = { save = { flags = {} } }
story.BILLS_HOUSE.onEnter(game, {})
check(entered == 1,
"entering Bill's House before meeting Bill starts Pikachu's reaction")
entered = 0
game.save.flags.EVENT_MET_BILL_2 = true
story.BILLS_HOUSE.onEnter(game, {})
check(entered == 0,
"Pikachu's Bill reaction does not replay after Bill is met")
package.loaded["src.world.PikachuFollower"] = originalFollower
local GameVersion = require("src.core.GameVersion")
local PikachuFollower = require("src.world.PikachuFollower")
GameVersion.set("yellow")
local npc = {
pikachuFollower = true, cellX = 3, cellY = 8, px = 48, py = 128,
facing = "up",
}
local moves = {}
local yellowGame = {
save = { flags = {} },
data = {
pokemon = { PIKACHU = { spriteFront = "pikachu.png" } },
field = { emotionBubbles = {
bubbles = {
{ name = "QUESTION_BUBBLE" }, { name = "EXCLAMATION_BUBBLE" },
},
} },
},
}
local ow = {
map = { id = "BILLS_HOUSE" }, npcs = { npc }, entities = { npc },
player = { cellX = 3, cellY = 7 },
scriptMove = function(_, entity, dir, tiles, onDone)
moves[#moves + 1] = { entity = entity, dir = dir, tiles = tiles,
onDone = onDone }
end,
}
PikachuFollower.onBillsHouseEnter(yellowGame, ow)
check(ow.pikachuBillsScene and #moves == 1
and moves[1].entity == npc and moves[1].dir == "right"
and moves[1].tiles == 3,
"Bill's House entry parks Pikachu and walks it to Bill")
moves[1].onDone()
check(#moves == 2 and moves[2].dir == "up" and moves[2].tiles == 1,
"Pikachu finishes its cartridge entry route beside Bill")
moves[2].onDone()
check(ow.emote and ow.emote.bubble == 1,
"Pikachu shows its confused reaction after reaching Bill")
ow.player.facing = "down"
PikachuFollower.onBillEnteredMachine(yellowGame, ow)
check(#moves == 3 and moves[3].dir == "up" and moves[3].tiles == 3,
"Pikachu walks to the cell separator after Bill enters it")
moves[3].onDone()
check(ow.emote and ow.emote.bubble == 1,
"Pikachu wonders at the cell separator")
npc.goalX, npc.goalY = 9, 9
PikachuFollower.update(yellowGame, ow)
check(npc.goalX == 9 and npc.goalY == 9,
"Pikachu stays parked in Bill's House during the scene")
PikachuFollower.onBillExitedMachine(yellowGame, ow)
check(ow.emote and ow.emote.bubble == 2 and npc.facing == "left",
"Pikachu reacts when Bill comes back out")
GameVersion.set("red")
S.finish()
+28
View File
@@ -431,6 +431,34 @@ do
for _, bak in ipairs(FsIo.globPrefix(tmpPath .. ".bak-")) do os.remove(bak) end
end
do
-- #476: on Android a high-DPI 1560x720 capture can leave the editor with
-- only a compact logical viewport. The Items picker must not hand a
-- negative list height to love.graphics.setScissor in that layout.
local tmpPath = os.tmpname() .. "-items-compact-save.lua"
local f = io.open(tmpPath, "wb")
f:write(SaveData.encode(SaveData.newGame()))
f:close()
local oldDimensions = love.graphics.getDimensions
local oldScissor = love.graphics.setScissor
love.graphics.getDimensions = function() return 520, 240 end
love.graphics.setScissor = function(_, _, width, height)
if width and (width < 0 or height < 0) then
error("Can't set scissor with negative width and/or height.")
end
end
App.load(tmpPath, { version = "red" })
App.getState().tab = "items"
local ok, err = pcall(App.draw)
check(ok, "the Items tab draws in a compact Android viewport: " .. tostring(err))
love.graphics.getDimensions = oldDimensions
love.graphics.setScissor = oldScissor
os.remove(tmpPath)
for _, bak in ipairs(FsIo.globPrefix(tmpPath .. ".bak-")) do os.remove(bak) end
end
do
-- Whole-editor smoke test: every tab has to survive a real headless draw,
-- which is what catches a layout that divides by a nil font metric or
+1
View File
@@ -3278,6 +3278,7 @@ runSuites(orderedGlob("tests/parity_*.lua", {
"tests/parity_static.lua", "tests/parity_trashcans.lua",
"tests/parity_hof.lua", "tests/parity_trade_gift.lua",
"tests/parity_yellow_trades.lua",
"tests/parity_yellow_bills_pikachu.lua",
"tests/parity_intro.lua", "tests/parity_tilt.lua",
"tests/parity_gbcfx.lua",
}))
+12 -3
View File
@@ -381,13 +381,22 @@ end
-- Clip drawing to a rect (list bodies). No-ops under the headless stub.
function Kit.pushClip(x, y, w, h)
if G and G.setScissor then
G.setScissor(math.floor(x), math.floor(y), math.ceil(w), math.ceil(h))
-- A compact mobile viewport can leave a panel with no room for a list.
-- LÖVE rejects negative scissor dimensions, so treat an exhausted clip
-- region as empty instead of passing invalid geometry through to it.
Kit._clipActive = G and G.setScissor ~= nil
if Kit._clipActive then
if w <= 0 or h <= 0 then
G.setScissor(0, 0, 0, 0)
else
G.setScissor(math.floor(x), math.floor(y), math.ceil(w), math.ceil(h))
end
end
end
function Kit.popClip()
if G and G.setScissor then G.setScissor() end
if Kit._clipActive and G and G.setScissor then G.setScissor() end
Kit._clipActive = false
end
return Kit