mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 08:51:27 +02:00
Merge pull request #1798 from campavao/rfc-0015-battle-rule-hooks
engine: battle.style and catch.nickname rule hooks
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
-- A sandboxed mod can decide two battle rules the OPTION screen and the cart
|
||||
-- otherwise decide for the player -- whether a faint offers a free switch
|
||||
-- (battle.style) and whether a catch asks for a nickname (catch.nickname) --
|
||||
-- using only public mod surfaces, and neither hook touches the player's
|
||||
-- saved preference.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local FIXTURE = {
|
||||
["mods/rules_probe/manifest.json"] = [[{
|
||||
"id": "rules_probe",
|
||||
"name": "Rules Probe",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"api": 2
|
||||
}]],
|
||||
["mods/rules_probe/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.exports.style = "set"
|
||||
mod.exports.nickname = false
|
||||
mod.hooks:wrap("battle.style", function(next, battle)
|
||||
mod.exports.styleCalls = (mod.exports.styleCalls or 0) + 1
|
||||
mod.exports.styleBattle = battle
|
||||
if mod.exports.style ~= nil then return mod.exports.style end
|
||||
return next(battle)
|
||||
end)
|
||||
mod.hooks:wrap("catch.nickname", function(next, mon, ctx)
|
||||
mod.exports.nameCalls = (mod.exports.nameCalls or 0) + 1
|
||||
mod.exports.nameCtx = ctx
|
||||
if mod.exports.nickname ~= nil then return mod.exports.nickname end
|
||||
return next(mon, ctx)
|
||||
end)
|
||||
]],
|
||||
}
|
||||
|
||||
local function fixtureBattle(style)
|
||||
local data = { pokemon = {}, text = {} }
|
||||
return setmetatable({
|
||||
game = { save = { options = { battleStyle = style } },
|
||||
stack = { push = function() end }, data = data },
|
||||
data = data,
|
||||
queue = {}, nextInsert = 0,
|
||||
}, { __index = BattleState })
|
||||
end
|
||||
|
||||
-- does the queued UI, if any, put the yes/no prompt on screen?
|
||||
local function promptQueued(battle)
|
||||
for _, item in ipairs(battle.queue) do
|
||||
if item.ui then
|
||||
local state = item.ui()
|
||||
if getmetatable(state) == TextBox and state.choice then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- ------- no mod: the OPTION row and the cart's AskName decide
|
||||
|
||||
local vanilla = T.sdk.loadNone({})
|
||||
T.eq(fixtureBattle("shift"):battleStyle(), "shift", "no mod: SHIFT row reads shift")
|
||||
T.eq(fixtureBattle("set"):battleStyle(), "set", "no mod: SET row reads set")
|
||||
T.eq(fixtureBattle("SET"):battleStyle(), "set", "no mod: the row is case-insensitive")
|
||||
T.eq(fixtureBattle(nil):battleStyle(), "shift", "no mod: a missing row is the cart default")
|
||||
|
||||
local plain = fixtureBattle("shift")
|
||||
local mon = { species = "RATTATA" }
|
||||
T.eq(plain:offerNickname(mon, "RATTATA"), true, "no mod: a catch queues the prompt")
|
||||
T.eq(promptQueued(plain), true, "no mod: and it is the yes/no box")
|
||||
T.eq(mon.nickname, nil, "no mod: nothing is named behind the player's back")
|
||||
vanilla.release()
|
||||
|
||||
-- ------- a mod answers both
|
||||
|
||||
local run = T.sdk.loadMods({ "mods/rules_probe" }, { fs = T.sdk.memfs(FIXTURE) })
|
||||
T.eq(#run.errors, 0, "the rules probe loads clean (" .. tostring(run.errors[1]) .. ")")
|
||||
local probe = run.loader.exports.rules_probe
|
||||
|
||||
local forced = fixtureBattle("shift")
|
||||
T.eq(forced:battleStyle(), "set", "\"set\" wins over a SHIFT row")
|
||||
T.eq(forced.game.save.options.battleStyle, "shift", "without writing the row")
|
||||
T.eq(probe.styleCalls, 1, "the hook ran once")
|
||||
T.check(probe.styleBattle == forced, "and was handed the battle")
|
||||
probe.style = "shift"
|
||||
T.eq(fixtureBattle("set"):battleStyle(), "shift", "\"shift\" wins over a SET row")
|
||||
probe.style = "banana"
|
||||
T.eq(fixtureBattle("set"):battleStyle(), "set",
|
||||
"an answer that is neither reads as the row")
|
||||
probe.style = nil
|
||||
T.eq(fixtureBattle("set"):battleStyle(), "set", "falling through reads the row")
|
||||
|
||||
local skipped = fixtureBattle("shift")
|
||||
local kept = { species = "RATTATA" }
|
||||
T.eq(skipped:offerNickname(kept, "RATTATA"), false, "false: no prompt is queued")
|
||||
T.eq(#skipped.queue, 0, "nothing at all is queued")
|
||||
T.eq(kept.nickname, nil, "and the species name is kept")
|
||||
T.eq(probe.nameCalls, 1, "the hook ran once")
|
||||
T.check(probe.nameCtx and probe.nameCtx.battle == skipped, "with the battle in ctx")
|
||||
T.eq(probe.nameCtx and probe.nameCtx.name, "RATTATA", "and the display name")
|
||||
|
||||
probe.nickname = "SPIKE"
|
||||
local named = { species = "RATTATA" }
|
||||
T.eq(fixtureBattle("shift"):offerNickname(named, "RATTATA"), false,
|
||||
"a string: no prompt either")
|
||||
T.eq(named.nickname, "SPIKE", "and it is the nickname")
|
||||
|
||||
probe.nickname = "TOOLONGFORTHEGRID"
|
||||
local clipped = { species = "RATTATA" }
|
||||
fixtureBattle("shift"):offerNickname(clipped, "RATTATA")
|
||||
T.eq(clipped.nickname, "TOOLONGFOR", "a long string is clipped to the grid's ten")
|
||||
|
||||
probe.nickname = ""
|
||||
local blank = { species = "RATTATA" }
|
||||
T.eq(fixtureBattle("shift"):offerNickname(blank, "RATTATA"), false,
|
||||
"an empty string still declines the prompt")
|
||||
T.eq(blank.nickname, nil, "and names nothing, like the grid's own empty entry")
|
||||
|
||||
probe.nickname = nil
|
||||
local asked = fixtureBattle("shift")
|
||||
T.eq(asked:offerNickname({ species = "RATTATA" }, "RATTATA"), true,
|
||||
"falling through asks as usual")
|
||||
T.eq(promptQueued(asked), true, "with the real prompt")
|
||||
|
||||
run.release()
|
||||
T.finish("battle rule hooks")
|
||||
Reference in New Issue
Block a user