Merge branch 'dev' into feat/switch-nx

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-02 22:30:18 -03:00
4 changed files with 175 additions and 14 deletions
+8 -3
View File
@@ -981,10 +981,15 @@ M.VICTORY_ROAD_3F = {
local championsRoomRivalScript = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
{ "jump_if_true", 25 }, -- 3 past end
{ "jump_if_true", 26 }, -- 3 past end
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
{ "jump_if_false", 25 }, -- 6 past end
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
-- the intro text, before the battle itself (#706); pushBattle's wipe-time
-- playBattle("final") then no-ops on the same song, so the theme stays
-- continuous into the fight
{ "play_music", "Music_FinalBattle" }, -- 5
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 6
{ "jump_if_false", 26 }, -- 6 past end
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
+17 -2
View File
@@ -3540,8 +3540,16 @@ function BattleState:onFaint(battler)
self:actNext(function()
battler.fainted = true
local Sound = require("src.core.Sound")
Sound.playCry(self.data, battler.mon.species)
Sound.play(self.data, "Faint_Fall")
if battler.isPlayer then
-- RemoveFaintedPlayerMon (core.asm:1040-1042): the player mon's
-- faint plays its ordinary species cry -- no Faint_Fall
Sound.playCry(self.data, battler.mon.species)
elseif self.kind ~= "wild" then
-- FaintEnemyPokemon (core.asm:732-771): the enemy faint plays no
-- species cry; trainer battles get SFX_FAINT_FALL, then SFX_FAINT_THUD
-- once it finishes (wild battles skip straight to the victory music)
Sound.play(self.data, "Faint_Fall")
end
self.fx = self.fx or {}
-- SlideDownFaintedMonPic: PIC_HEIGHT (7) slide steps, each closing with
-- DelayFrames 2 (core.asm:1186-1222). The port held this one twice as
@@ -3550,6 +3558,13 @@ function BattleState:onFaint(battler)
end)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { wait = Timing.FAINT_SLIDE })
if not battler.isPlayer and self.kind ~= "wild" then
-- FaintEnemyPokemon's SFX_FAINT_THUD lands as the slide does (after
-- Faint_Fall, before EnemyMonFaintedText)
self:actNext(function()
require("src.core.Sound").play(self.data, "Faint_Thud")
end)
end
if not battler.isPlayer and self.kind == "wild" then
-- FaintEnemyPokemon .wild_win (core.asm:792-795): beating a wild
-- mon calls EndLowHealthAlarm and starts MUSIC_DEFEATED_WILD_MON
+30 -9
View File
@@ -542,6 +542,15 @@ function RomImporter:_romAction(version)
else self:choose(version) end
end
-- Sanitize a string before it is interpolated into a picker shell command:
-- * "%" would be eaten as a string.format directive (#665);
-- * '"' would break the AppleScript / zenity double-quoted argument and
-- "'" the surrounding single-quoted shell string.
local function shellSafe(s)
s = tostring(s):gsub("%%", "%%%%")
return s:gsub('"', '\\"'):gsub("'", "''")
end
-- LOVE 11.5 on Android has no native file picker (love.window.showFileDialog
-- is a LOVE 12 nightly-only addition) and never fires love.filedropped, so
-- neither desktop path below works there. conf.lua points the Android save
@@ -648,7 +657,7 @@ end
local function chooseRom(promptName)
promptName = promptName or "Pokemon"
local prompt = "Choose your " .. promptName .. " ROM"
local prompt = shellSafe("Choose your " .. promptName .. " ROM")
local platform = love.system.getOS()
if platform == "OS X" then
return commandOutput(
@@ -660,10 +669,16 @@ local function chooseRom(promptName)
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
"$d.Title='" .. prompt .. "';",
"$d.Filter='Game Boy ROM (*.gb;*.gbc)|*.gb;*.gbc|All files (*.*)|*.*';",
-- write the pick as UTF-8: the console's OEM codepage would mangle
-- non-ASCII names (Pokémon -> Pok\x82mon) and crash any text draw
-- that shows them (#325)
"if($d.ShowDialog() -eq 'OK'){[Console]::OutputEncoding=[Text.Encoding]::UTF8; [Console]::Write($d.FileName)}",
-- copy the pick to a plain-ASCII temp name and answer with that:
-- the console's OEM codepage would mangle a non-ASCII path
-- (Pokémon -> Pok\x82mon) and io.open on Windows needs ANSI bytes,
-- so returning the original name both crashed the notice draw and
-- could never have opened the file (#325, #665)
"if($d.ShowDialog() -eq 'OK'){",
"$t=Join-Path $env:TEMP 'pokeport_rom_pick.gb';",
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
"[Console]::Write($t)}",
})
return commandOutput(
'powershell -NoProfile -STA -Command "' .. script .. '"')
@@ -682,7 +697,7 @@ end
-- Returns the chosen absolute path or nil. Android uses love.system.pickFile
-- ("mod") instead -- see RomImporter:chooseMod.
local function chooseZip()
local prompt = Strings("Choose a mod .zip")
local prompt = shellSafe(Strings("Choose a mod .zip"))
local platform = love.system.getOS()
if platform == "OS X" then
return commandOutput(
@@ -722,7 +737,7 @@ end
-- dialogs). Returns the chosen absolute path or nil. Android uses
-- love.system.pickFile("sav") instead -- see RomImporter:chooseSaveImport.
local function chooseSav()
local prompt = Strings("Choose a .sav save file")
local prompt = shellSafe(Strings("Choose a .sav save file"))
local platform = love.system.getOS()
if platform == "OS X" then
return commandOutput(
@@ -734,8 +749,14 @@ local function chooseSav()
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
"$d.Title='" .. prompt .. "';",
"$d.Filter='Game Boy save (*.sav)|*.sav|All files (*.*)|*.*';",
-- UTF-8, like the ROM and mod pickers (#325)
"if($d.ShowDialog() -eq 'OK'){[Console]::OutputEncoding=[Text.Encoding]::UTF8; [Console]::Write($d.FileName)}",
-- copy the pick to a plain-ASCII temp name: io.open on Windows
-- needs ANSI bytes, so a non-ASCII path (Pokémon -> Pok\x82mon)
-- could never have been opened (#325, #665)
"if($d.ShowDialog() -eq 'OK'){",
"$t=Join-Path $env:TEMP 'pokeport_sav_pick.sav';",
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
"[Console]::Write($t)}",
})
return commandOutput(
'powershell -NoProfile -STA -Command "' .. script .. '"')
+120
View File
@@ -0,0 +1,120 @@
-- Parity test: the faint sound sequence matches pokered, per side.
--
-- RemoveFaintedPlayerMon (engine/battle/core.asm:1003-1045): the player
-- mon's faint plays its ordinary species cry (PlayCry) -- no Faint_Fall.
--
-- FaintEnemyPokemon (engine/battle/core.asm:732-796): the enemy faint
-- plays NO species cry; trainer battles play SFX_FAINT_FALL, wait for it
-- to finish, then SFX_FAINT_THUD. Wild battles skip both and go
-- straight to the victory music (.wild_win).
--
-- The port previously 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 (#709).
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Data = T.fixtures.fresh()
local Font = require("src.render.Font")
Font.load(Data)
local TypeChart = require("src.battle.TypeChart")
TypeChart.load(Data)
local Pokemon = require("src.pokemon.Pokemon")
local SaveData = require("src.core.SaveData")
local BattleState = require("src.battle.BattleState")
local Sound = require("src.core.Sound")
-- record cries and sfx instead of sounding them
local cries, sfx = {}, {}
Sound.playCry = function(_, species) cries[#cries + 1] = species end
Sound.play = function(_, name) sfx[#sfx + 1] = name end
local function freshGame()
local save = SaveData.newGame()
save.player.name = "RED"
save.player.rival = "GARY"
save.party = { Pokemon.new(Data, "FIXMON_A", 30) }
return { data = Data, save = save,
input = { wasPressed = function() return false end,
wasJustPressed = function() return false end },
stack = { top = function() return nil end,
push = function() end, pop = function() end } }
end
local function reset()
cries, sfx = {}, {}
end
-- pump the queue until it drains or the faint sounds have all fired (the
-- faint's sounds are actNext rows ahead of the faint text, which needs a
-- text input stub this driver does not bother to provide)
local function pump(battle, expectedSounds)
local count = expectedSounds or 0
local seen = 0
for _ = 1, 60 do
local before = #sfx
local ok = pcall(battle.updateQueue, battle)
if not ok then return false end
if #sfx > before then seen = #sfx end
if seen >= count then return true end
end
return false
end
-- player mon faint: only the species cry, no Faint_Fall
do
reset()
local game = freshGame()
local battle = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
battle.participants = {}
battle.playVictoryMusic = function() end
battle:onFaint(battle.player)
pump(battle, 1)
T.eq(cries[1], "FIXMON_A", "the player mon's faint plays its species cry")
T.eq(#cries, 1, "no other cry on the player faint")
for _, name in ipairs(sfx) do
T.check(name ~= "Faint_Fall",
"the player faint never plays Faint_Fall (#709)")
end
end
-- enemy faint, trainer battle: Faint_Fall then Faint_Thud, no species cry
do
reset()
local game = freshGame()
local battle = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
battle.participants = {}
battle.playVictoryMusic = function() end
battle:onFaint(battle.enemy)
pump(battle, 2)
T.eq(#cries, 0, "the enemy faint plays no species cry")
local fall, thud = false, false
for i, name in ipairs(sfx) do
if name == "Faint_Fall" then
T.check(not fall, "Faint_Fall plays once")
fall = true
T.check(not thud, "Faint_Fall precedes Faint_Thud")
elseif name == "Faint_Thud" then
thud = true
end
end
T.check(fall and thud, "trainer enemy faint plays Faint_Fall and Faint_Thud")
end
-- enemy faint, wild battle: no faint sfx at all (victory music only)
do
reset()
local game = freshGame()
local battle = BattleState.newWild(game, "FIXMON_B", 5)
battle.participants = {}
battle.playVictoryMusic = function() end
battle:onFaint(battle.enemy)
pump(battle)
T.eq(#cries, 0, "the wild enemy faint plays no species cry")
for _, name in ipairs(sfx) do
T.check(name ~= "Faint_Fall" and name ~= "Faint_Thud",
"the wild enemy faint plays no faint sfx (.wild_win)")
end
end
T.finish("parity faint cry bug709")