mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
Merge pull request #1434 from sanjinpepic/upstream-fixes
This commit is contained in:
@@ -131,4 +131,25 @@ T.same(Checkpoint.inspect(game), {
|
||||
canCapture = true, canRestore = true, kind = "overworld",
|
||||
}, "settled overworld remains supported")
|
||||
|
||||
-- drainHold gates capture (see the refused() case above) exactly because it
|
||||
-- marks an HP bar mid-animation. Once stepHPDrain settles the bar it must
|
||||
-- let go of that gate too, or the very first drain of a battle leaves the
|
||||
-- checkpoint contract refused for everything after it.
|
||||
do
|
||||
local game3, _, battle3 = makeGame()
|
||||
battle3.enemy.mon.hp = battle3.enemy.mon.hp - 5
|
||||
local frames = 0
|
||||
while battle3:stepHPDrain() and frames < 10000 do
|
||||
frames = frames + 1
|
||||
end
|
||||
T.eq(battle3.enemy.shownHP, battle3.enemy.mon.hp,
|
||||
"the HP bar settles on the new total")
|
||||
T.eq(battle3.enemy.drainHold, nil,
|
||||
"drainHold releases the checkpoint gate once the bar finishes draining")
|
||||
local capability = Checkpoint.inspect(game3)
|
||||
T.check(capability.canCapture == true,
|
||||
"a checkpoint is capturable again after the drain settles: "
|
||||
.. tostring(capability.reason))
|
||||
end
|
||||
|
||||
T.finish()
|
||||
|
||||
@@ -468,6 +468,32 @@ do
|
||||
T.eq(forced.shiny, true, "opts.shiny still wins over shiny.roll")
|
||||
end)
|
||||
|
||||
-- Mon.syncIdentity (wired into refreshStats, which SummaryMenu.new calls
|
||||
-- on every menu open) used to recompute mon.shiny from DVs unconditionally,
|
||||
-- so opening the summary screen on a forced shiny -- one whose DVs do not
|
||||
-- happen to match the natural pattern -- un-shinied it the moment the menu
|
||||
-- opened. shiny is monotonic once true: a natural roll or a forced one
|
||||
-- both stay shiny through any later refresh, the way opts.shiny already
|
||||
-- wins at construction.
|
||||
do
|
||||
local forced = Mon.new(DATA, "SEEDMON", 5, { dvs = plainDvs, shiny = true })
|
||||
T.eq(forced.shiny, true, "still shiny straight out of Mon.new")
|
||||
Mon.syncIdentity(forced, DATA)
|
||||
T.eq(forced.shiny, true, "syncIdentity does not clobber a forced shiny")
|
||||
Mon.refreshStats(forced, DATA)
|
||||
T.eq(forced.shiny, true,
|
||||
"refreshStats (SummaryMenu.new's call) does not either")
|
||||
|
||||
-- the natural cases are unaffected: DVs that read shiny stay shiny,
|
||||
-- DVs that do not stay plain
|
||||
local natural = Mon.new(DATA, "SEEDMON", 5, { dvs = shinyDvs })
|
||||
Mon.syncIdentity(natural, DATA)
|
||||
T.eq(natural.shiny, true, "a naturally shiny mon still reads shiny")
|
||||
local plain = Mon.new(DATA, "SEEDMON", 5, { dvs = plainDvs })
|
||||
Mon.syncIdentity(plain, DATA)
|
||||
T.eq(plain.shiny, false, "a plain mon is not promoted to shiny")
|
||||
end
|
||||
|
||||
local genderCtx
|
||||
withHook("gender.roll", function(nextFn, ctx)
|
||||
genderCtx = ctx
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- ItemEffects.use crashed instead of refusing when a species record carried
|
||||
-- no tmhm list at all (ipairs(nil)), which is a different situation from a
|
||||
-- species whose list simply does not name the move being taught -- that
|
||||
-- case already refuses cleanly with MonCannotLearnMachineMoveText. A mod
|
||||
-- species missing the field entirely took the whole game down on the very
|
||||
-- first TM/HM use rather than reaching that refusal.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.harness").suite("item effects tmhm nil")
|
||||
local Fixtures = require("tests.modkit.fixtures")
|
||||
local ItemEffects = require("src.inventory.ItemEffects")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
|
||||
local Data = Fixtures.fresh()
|
||||
Data.pokemon.FIXMON_A.tmhm = nil
|
||||
|
||||
local mon = Pokemon.new(Data, "FIXMON_A", 10)
|
||||
local save = { player = { name = "RED" } }
|
||||
|
||||
local ok, result, payload = pcall(ItemEffects.use, Data, save, "FIX_TM", mon)
|
||||
T.check(ok, "using a TM on a species with no tmhm list does not crash: "
|
||||
.. tostring(result))
|
||||
if ok then
|
||||
T.eq(result, "failed", "the species refuses the move instead of crashing into it")
|
||||
T.check(type(payload) == "table" and payload[1] ~= nil,
|
||||
"a refusal message is still returned")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,101 @@
|
||||
-- Public mod-API coverage for the "item.use" hook (src/ui/BagMenu.lua).
|
||||
--
|
||||
-- Before this hook existed, every result ItemEffects.use returned fell
|
||||
-- through to one unconditional call with nothing wrapped around it: a mod
|
||||
-- could not suppress a message, delay it behind a screen of its own, or
|
||||
-- replace what a specific item id does after the bag decides to use it.
|
||||
-- This exercises the seam end to end through the public mod API -- a real
|
||||
-- BagMenu list, a real USE selection -- rather than calling the hook
|
||||
-- machinery directly.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Bag = require("src.inventory.Bag")
|
||||
|
||||
-- Real TextBoxes want a Font atlas; this only cares that useOn reaches the
|
||||
-- no-effect fallthrough, so the same stand-in tests/parity_rare_candy_menu.lua
|
||||
-- uses for a ROM-backed run works here too.
|
||||
local realTextBox = package.loaded["src.render.TextBox"]
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, done) return { textBox = true, text = text, done = done } end,
|
||||
}
|
||||
package.loaded["src.ui.BagMenu"] = nil
|
||||
local BagMenu = require("src.ui.BagMenu")
|
||||
|
||||
local FIXTURE = {
|
||||
["mods/item_hook_probe/manifest.json"] = [[{
|
||||
"id": "item_hook_probe",
|
||||
"name": "Item Hook Probe",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"api": 2
|
||||
}]],
|
||||
["mods/item_hook_probe/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.hooks:wrap("item.use",
|
||||
function(vanilla, game, battle, id, target, list, moveIndex, picker)
|
||||
mod.exports.calls = (mod.exports.calls or 0) + 1
|
||||
mod.exports.id = id
|
||||
mod.exports.battle = battle
|
||||
mod.exports.target = target
|
||||
return vanilla(game, battle, id, target, list, moveIndex, picker)
|
||||
end)
|
||||
]],
|
||||
}
|
||||
|
||||
local function newStack()
|
||||
local stack = { states = {} }
|
||||
function stack:push(s) self.states[#self.states + 1] = s end
|
||||
function stack:pop() return table.remove(self.states) end
|
||||
function stack:top() return self.states[#self.states] end
|
||||
return stack
|
||||
end
|
||||
|
||||
local run = T.sdk.loadMods({ "mods/item_hook_probe" }, {
|
||||
fs = T.sdk.memfs(FIXTURE),
|
||||
})
|
||||
T.eq(#run.errors, 0,
|
||||
"the probe mod loads clean (" .. tostring(run.errors[1]) .. ")")
|
||||
|
||||
local game = {
|
||||
data = run.data,
|
||||
stack = newStack(),
|
||||
save = {
|
||||
player = { name = "RED" }, inventory = {}, money = 0,
|
||||
options = { battleStyle = "set", battleAnim = "on" },
|
||||
pokedex = { seen = {}, owned = {} }, flags = {},
|
||||
},
|
||||
}
|
||||
Bag.add(game.save, "FIX_POTION", 1)
|
||||
|
||||
local list = BagMenu.new(game, {})
|
||||
game.stack:push(list)
|
||||
local row
|
||||
for i, r in ipairs(list.items) do
|
||||
if r.value == "FIX_POTION" then row = i end
|
||||
end
|
||||
T.check(row ~= nil, "the fixture item is in the bag")
|
||||
list.index = row
|
||||
list.onChoose(list.items[row], list)
|
||||
|
||||
-- out of battle the bag offers USE / TOSS first (start_sub_menus.asm)
|
||||
local sub = game.stack:top()
|
||||
T.check(sub ~= nil and sub.items and sub.items[1] and sub.items[1].onSelect,
|
||||
"the USE/TOSS submenu opened")
|
||||
sub.items[1].onSelect()
|
||||
|
||||
local out = run.loader.exports.item_hook_probe or {}
|
||||
T.eq(out.calls, 1, "the hook fires exactly once for a bag item use")
|
||||
T.eq(out.id, "FIX_POTION", "the hook sees the item id")
|
||||
T.eq(out.battle, nil, "the hook sees the field-use battle argument (nil)")
|
||||
|
||||
local top = game.stack:top()
|
||||
T.check(type(top) == "table" and top.textBox == true,
|
||||
"vanilla still ran: the no-effect message box landed on the stack")
|
||||
|
||||
run.release()
|
||||
package.loaded["src.render.TextBox"] = realTextBox
|
||||
package.loaded["src.ui.BagMenu"] = nil
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,89 @@
|
||||
-- A map object's `pokemon` field (the static wild encounter kind --
|
||||
-- OverworldController.lua's `d.pokemon`, handed straight to
|
||||
-- BattleState.newWild with no existence check of its own) used to go
|
||||
-- completely unchecked: R.maps.objects was f.opt(f.list(f.any)), so a
|
||||
-- typo'd species sat in a loaded mod and only surfaced as a crash the
|
||||
-- moment a player stepped up to that object. Every other kind sharing the
|
||||
-- objects array (NPCs, signs-as-objects, warps) has fields this schema
|
||||
-- still does not know about, which is what f.partial is for: it types only
|
||||
-- `pokemon` and leaves the rest of an object's shape alone.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
|
||||
local function manifest(id)
|
||||
return ([[{
|
||||
"id": "%s", "name": "%s", "version": "1.0.0",
|
||||
"entry": "main.lua", "api": 2
|
||||
}]]):format(id, id)
|
||||
end
|
||||
|
||||
-- ------- a bad species id is caught as a load error, not left to crash
|
||||
|
||||
local BAD = {
|
||||
["mods/bad_static_encounter/manifest.json"] = manifest("bad_static_encounter"),
|
||||
["mods/bad_static_encounter/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.content.maps:patch("FIX_ROUTE", {
|
||||
objects = {
|
||||
{ pokemon = "NOT_A_SPECIES", level = 30, text = "Gyaoo!" },
|
||||
},
|
||||
})
|
||||
]],
|
||||
}
|
||||
|
||||
do
|
||||
local run = T.sdk.loadMods({ "mods/bad_static_encounter" },
|
||||
{ fs = T.sdk.memfs(BAD) })
|
||||
local dangling = {}
|
||||
for _, message in ipairs(run.errors) do
|
||||
if message:match("unresolved reference") then
|
||||
dangling[#dangling + 1] = message
|
||||
end
|
||||
end
|
||||
T.eq(#dangling, 1,
|
||||
"a bad static-encounter species is reported once ("
|
||||
.. table.concat(dangling, "; ") .. ")")
|
||||
T.check(dangling[1] and dangling[1]:match("maps%.FIX_ROUTE%.objects")
|
||||
and dangling[1]:match("pokemon"),
|
||||
"the report names the map, the objects field and the pokemon registry: "
|
||||
.. tostring(dangling[1]))
|
||||
run.release()
|
||||
end
|
||||
|
||||
-- ------- a real species resolves, and an NPC-shaped object beside it (no
|
||||
-- pokemon field at all, and fields this schema never named -- sprite,
|
||||
-- movement, range) is untouched
|
||||
|
||||
local GOOD = {
|
||||
["mods/good_static_encounter/manifest.json"] = manifest("good_static_encounter"),
|
||||
["mods/good_static_encounter/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.content.maps:patch("FIX_ROUTE", {
|
||||
objects = {
|
||||
{ index = 1, name = "FIXROUTE_TRAINER", sprite = "SPRITE_FIX_NPC",
|
||||
movement = "STAY", range = "NONE", text = "TEXT_FIXROUTE_TRAINER",
|
||||
x = 5, y = 9 },
|
||||
{ pokemon = "FIXMON_A", level = 30, text = "Gyaoo!" },
|
||||
},
|
||||
})
|
||||
]],
|
||||
}
|
||||
|
||||
do
|
||||
local run = T.sdk.loadMods({ "mods/good_static_encounter" },
|
||||
{ fs = T.sdk.memfs(GOOD) })
|
||||
T.eq(#run.errors, 0,
|
||||
"a real species and an untyped NPC object both load clean ("
|
||||
.. tostring(run.errors[1]) .. ")")
|
||||
local objects = run.data.maps.FIX_ROUTE.objects
|
||||
T.eq(#objects, 2, "both objects landed on the map")
|
||||
T.eq(objects[1].sprite, "SPRITE_FIX_NPC",
|
||||
"the NPC object's untyped fields passed through unexamined")
|
||||
T.eq(objects[2].pokemon, "FIXMON_A",
|
||||
"the static encounter's species field passed through too")
|
||||
run.release()
|
||||
end
|
||||
|
||||
T.finish()
|
||||
Reference in New Issue
Block a user