mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 16:00:53 +02:00
368 lines
14 KiB
Lua
368 lines
14 KiB
Lua
-- Driver: the two edges the capture mode has to hold on to.
|
|
--
|
|
-- A FULL with an EMPTY BAG. The encounter must still be a Let's Go
|
|
-- encounter -- head-on seat, no player Pokemon, no classic menu --
|
|
-- with the readout saying there is nothing to throw and RUN as the
|
|
-- way out. The failure this catches is the old behaviour: falling
|
|
-- back to the battle menu, which under FULL offers a FIGHT against a
|
|
-- foe that never takes a turn.
|
|
--
|
|
-- B FULL, running DRY MID-ENCOUNTER. One ball, thrown weakly enough to
|
|
-- fall short: the miss must land in the empty hand rather than
|
|
-- tearing the session down.
|
|
--
|
|
-- C The SCRIPTED catch tutorial (the VIRIDIAN CITY old man; PROF.OAK
|
|
-- and the PIKACHU are the same makeOldManDemo). LET'S GO must not
|
|
-- touch it at any rung -- no session, no held camera, no veil -- and
|
|
-- it must play its scripted throw through to its own ending.
|
|
--
|
|
-- SHOT_DIR=.scratchpad/letsgo \
|
|
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/letsgo_empty.lua \
|
|
-- "/c/Program Files/LOVE/lovec.exe" .
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or ".scratchpad"
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local BattleState = require("src.battle.BattleState")
|
|
local Bag = require("src.inventory.Bag")
|
|
|
|
local exports = game.mods and game.mods.exports
|
|
local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib
|
|
if not lib then
|
|
U.log("DRAMATIC_SHAPE is not loaded -- enable it and run again")
|
|
return
|
|
end
|
|
local LetsGo = lib.require("LetsGo")
|
|
local CatchThrow = lib.require("CatchThrow")
|
|
local BattleScene = lib.require("BattleScene")
|
|
|
|
game.save.party = {
|
|
Pokemon.new(game.data, "CHARIZARD", 45),
|
|
Pokemon.new(game.data, "PIKACHU", 10),
|
|
}
|
|
game.save.player.name = "RED"
|
|
LetsGo.setting:setValue("full", game)
|
|
if os.getenv("DS_RUNG") == "cards" then
|
|
lib.require("OverworldBattle").setting:setValue(true, game)
|
|
U.log("3D-BTL forced to 2D-3D A")
|
|
end
|
|
U.log("LET'S GO mode: " .. tostring(LetsGo.mode()))
|
|
|
|
-- every ball out of the bag, whatever the save arrived with
|
|
local BALLS = { "POKE_BALL", "GREAT_BALL", "ULTRA_BALL", "MASTER_BALL" }
|
|
local function emptyBag()
|
|
for _, id in ipairs(BALLS) do
|
|
local n = game.save.inventory[id] or 0
|
|
if n > 0 then pcall(Bag.remove, game.save, id, n) end
|
|
game.save.inventory[id] = nil
|
|
end
|
|
end
|
|
local function ballCount()
|
|
local n = 0
|
|
for _, id in ipairs(BALLS) do n = n + (game.save.inventory[id] or 0) end
|
|
return n
|
|
end
|
|
|
|
-- A REAL key event, not U.tap's synthetic pressQueue inject. The bug
|
|
-- this driver has to be able to see (button edges read on the render
|
|
-- clock instead of the logic step) lives between love.keypressed and
|
|
-- whoever polls the edge, so a driver that writes the queue itself
|
|
-- jumps straight over it.
|
|
--
|
|
-- The B BUTTON's keyboard binding is "x" (or backspace) -- Input's
|
|
-- DEFAULT_BINDINGS. The `b = "b"` next to it is the GAMEPAD table, so a
|
|
-- driver that presses the "b" KEY presses nothing at all and reports a
|
|
-- dead button whatever the code does.
|
|
local B_KEY = "x"
|
|
local function key(name)
|
|
love.keypressed(name, name, false)
|
|
U.wait(2)
|
|
love.keyreleased(name, name)
|
|
U.wait(2)
|
|
end
|
|
|
|
U.teleport(game, "ROUTE_1", 5, 8, "down")
|
|
U.wait(90)
|
|
|
|
-- drive the intro chatter until the menu would open (which under FULL is
|
|
-- when capture mode takes over instead)
|
|
local function toCapture(battle)
|
|
U.wait(70)
|
|
for _ = 1, 80 do
|
|
if battle.phase == "menu" or CatchThrow.session() then break end
|
|
U.tap(game, "a")
|
|
U.wait(6)
|
|
end
|
|
U.wait(30)
|
|
end
|
|
|
|
-- Tap the fight all the way off the stack before the next case starts.
|
|
-- Cases that merely tapped A a fixed number of times left the previous
|
|
-- battle (and its session) alive whenever an outcome ran long, and the
|
|
-- next case then measured the leftover -- which reads as that case
|
|
-- failing, at a spot nowhere near the cause.
|
|
local function closeOut(battle)
|
|
for _ = 1, 400 do
|
|
if not CatchThrow.session() and game.stack:top() ~= battle then break end
|
|
U.tap(game, "a")
|
|
U.wait(4)
|
|
end
|
|
U.wait(40)
|
|
end
|
|
|
|
local function describe(tag, battle)
|
|
local s = CatchThrow.session()
|
|
U.log(("%s: session=%s empty=%s phase=%s battle=%s veil=%s hidePlayer=%s")
|
|
:format(tag, s and "yes" or "NO", s and tostring(s.empty) or "-",
|
|
s and s.phase or "-", tostring(battle.phase),
|
|
BattleScene.capture and "up" or "down",
|
|
tostring(BattleScene.capture
|
|
and BattleScene.capture.hidePlayer)))
|
|
return s
|
|
end
|
|
|
|
-- ------- A: an empty bag still gets the capture screen
|
|
|
|
emptyBag()
|
|
U.log("== A: FULL with " .. ballCount() .. " balls")
|
|
local a = BattleState.newWild(game, "PIDGEY", 5)
|
|
a.onFinish = function() end
|
|
game.overworld:pushBattle(a)
|
|
toCapture(a)
|
|
|
|
local s = describe("A", a)
|
|
U.log(("A: hand is empty -> %s (want yes)"):format(
|
|
(s and s.empty) and "yes" or "NO"))
|
|
U.shot(game, DIR .. "/empty_1_aim.png")
|
|
|
|
-- and B is the way out: a Let's Go wild always escapes
|
|
key(B_KEY)
|
|
for _ = 1, 120 do
|
|
U.wait(2)
|
|
if a.result then break end
|
|
U.tap(game, "a")
|
|
end
|
|
U.log(("A: after B -- result=%s (want run)"):format(tostring(a.result)))
|
|
-- the session and the held camera are swept by battle.ended, which is the
|
|
-- teardown BELOW this, not the moment `result` is written -- so the sweep
|
|
-- is only worth asserting once the battle has actually left the stack
|
|
closeOut(a)
|
|
U.log(("A: after teardown -- session=%s veil=%s (want gone/down)")
|
|
:format(CatchThrow.session() and "still up" or "gone",
|
|
BattleScene.capture and "still up" or "down"))
|
|
|
|
-- ------- B: the LAST ball, thrown short
|
|
|
|
Bag.add(game.save, "POKE_BALL", 1, game.data)
|
|
CatchThrow.lastBall = "POKE_BALL"
|
|
U.log("== B: FULL with " .. ballCount() .. " ball")
|
|
local b = BattleState.newWild(game, "PIDGEY", 5)
|
|
b.onFinish = function() end
|
|
-- The outcome under test is what happens when the LAST ball MISSES, so
|
|
-- the roll must not be left to chance: a run where the Pidgey happened
|
|
-- to be caught used to cascade into every later case (the session lived
|
|
-- on into its epilogue, and C and D then measured that leftover instead
|
|
-- of their own). Forced failure, three shakes -- the roll is the
|
|
-- engine's business and is covered elsewhere.
|
|
b.catchAttempt = function() return false, 3 end
|
|
game.overworld:pushBattle(b)
|
|
toCapture(b)
|
|
describe("B", b)
|
|
|
|
-- a deliberately feeble swipe: it must fall short of the Pokemon, so
|
|
-- the miss is the outcome under test rather than a lucky catch
|
|
local aim = CatchThrow._aimInfo()
|
|
if aim then
|
|
local uw, uh = love.graphics.getDimensions()
|
|
local function toWin(gx, gy)
|
|
return (aim.lx + gx * aim.scale) * uw / aim.pw,
|
|
(aim.ly + gy * aim.scale) * uh / aim.ph
|
|
end
|
|
local hx, hy = aim.hand[1], aim.hand[2]
|
|
local dx, dy = aim.ring[1] - hx, aim.ring[2] - hy
|
|
local d = math.sqrt(dx * dx + dy * dy)
|
|
dx, dy = dx / d, dy / d
|
|
local step = 120 / 60
|
|
local px, py = toWin(hx, hy)
|
|
love.mousepressed(px, py, 1, false, 1)
|
|
for i = 1, 8 do
|
|
local wx, wy = toWin(hx + dx * step * i, hy + dy * step * i)
|
|
love.mousemoved(wx, wy, 0, 0, false)
|
|
U.wait(1)
|
|
end
|
|
local wx, wy = toWin(hx + dx * step * 8, hy + dy * step * 8)
|
|
love.mousereleased(wx, wy, 1, false, 1)
|
|
else
|
|
U.log("B: NO AIM INFO -- capture mode did not open")
|
|
end
|
|
|
|
-- ride the throw out, then keep tapping through the miss text until the
|
|
-- hand is refilled -- which, with the bag now empty, means the EMPTY hand
|
|
for _ = 1, 400 do
|
|
U.wait(2)
|
|
local q = CatchThrow.session()
|
|
if q and q.phase == "aim" and q.empty then break end
|
|
if not q then break end
|
|
if q.phase ~= "aim" and q.phase ~= "flight" then U.tap(game, "a") end
|
|
end
|
|
local sb = describe("B", b)
|
|
U.log(("B: last ball thrown, balls=%d -> %s (want an empty hand)")
|
|
:format(ballCount(),
|
|
(sb and sb.empty) and "empty hand" or
|
|
(sb and ("still holding " .. tostring(sb.ballId)))
|
|
or "SESSION GONE"))
|
|
U.shot(game, DIR .. "/empty_2_ranout.png")
|
|
key(B_KEY)
|
|
for _ = 1, 120 do
|
|
U.wait(2)
|
|
if b.result then break end
|
|
U.tap(game, "a")
|
|
end
|
|
U.log("B: after B -- result=" .. tostring(b.result) .. " (want run)")
|
|
closeOut(b)
|
|
|
|
-- ------- C: the scripted tutorial, untouched
|
|
|
|
Bag.add(game.save, "POKE_BALL", 10, game.data)
|
|
U.log("== C: the old man's demo, at FULL, with " .. ballCount() .. " balls")
|
|
local om = game.data.field.oldManBattle or { species = "WEEDLE", level = 5 }
|
|
local c = BattleState.newWild(game, om.species, om.level)
|
|
c:makeOldManDemo()
|
|
c.onFinish = function() end
|
|
U.log("C: LetsGo.scripted -> " .. tostring(LetsGo.scripted(c))
|
|
.. " fullWild -> " .. tostring(LetsGo.fullWild(c))
|
|
.. " wantsMinigame -> " .. tostring(LetsGo.wantsMinigame(c)))
|
|
game.overworld:pushBattle(c)
|
|
|
|
-- the demo drives ITSELF: the cursor, the bag and the throw are all
|
|
-- scripted, so this only watches. Any session or veil appearing here is
|
|
-- the failure.
|
|
local sawSession, sawVeil = false, false
|
|
local shotDemo = false
|
|
for i = 1, 900 do
|
|
if CatchThrow.session() then sawSession = true end
|
|
if BattleScene.capture then sawVeil = true end
|
|
if not shotDemo and i > 200 then
|
|
shotDemo = true
|
|
U.shot(game, DIR .. "/empty_3_oldman.png")
|
|
end
|
|
if c.result then break end
|
|
U.wait(2)
|
|
-- the scripted beats want A only to page the text along
|
|
if i % 3 == 0 then U.tap(game, "a") end
|
|
end
|
|
U.log(("C: session ever opened=%s (want no) veil ever up=%s (want no)")
|
|
:format(tostring(sawSession), tostring(sawVeil)))
|
|
U.log(("C: result=%s balls=%d (want 10 -- the demo consumes none)")
|
|
:format(tostring(c.result), ballCount()))
|
|
U.log(("C: party still %d, first is %s")
|
|
:format(#game.save.party,
|
|
game.data.pokemon[game.save.party[1].species].name))
|
|
|
|
-- ------- D: B RUNS, with a ball in hand, on a frame that runs several
|
|
-- logic steps
|
|
--
|
|
-- The reported failure, reproduced rather than reasoned about. Input:step
|
|
-- rebuilds the edge table once per FIXED step; Game:update runs the
|
|
-- frame's steps first and the render-clock hooks after, so any frame
|
|
-- carrying more than one step has already discarded the earlier steps'
|
|
-- edges. Below 60fps -- which is where a 3D battle lives -- that is
|
|
-- every press. speedOverride multiplies the logic clock, so it packs
|
|
-- several steps into each frame on demand and turns "sometimes, on a
|
|
-- slow machine" into "every time, here".
|
|
for _, speed in ipairs({ 1, 4 }) do
|
|
Bag.add(game.save, "POKE_BALL", 5, game.data)
|
|
CatchThrow.lastBall = "POKE_BALL"
|
|
game.speedOverride = speed > 1 and speed or nil
|
|
U.log(("== D: FULL, ball in hand, B to run at %dX logic speed"):format(speed))
|
|
local d = BattleState.newWild(game, "PIDGEY", 5)
|
|
d.onFinish = function() end
|
|
game.overworld:pushBattle(d)
|
|
toCapture(d)
|
|
local sd = describe("D" .. speed, d)
|
|
if sd and not sd.empty then
|
|
-- where does the key actually get to? Each stage of the chain, so a
|
|
-- dead B is attributed rather than guessed at
|
|
local top = game.stack and game.stack:top()
|
|
U.log(("D%dX probe: top=%s onKeyPressed=%s fullWild=%s declinable=%s")
|
|
:format(speed, tostring(top and top.screenId or "?"),
|
|
tostring(top and top.onKeyPressed ~= nil),
|
|
tostring(sd.fullWild), tostring(sd.declinable)))
|
|
-- ------- the counter-factual, measured rather than argued
|
|
--
|
|
-- How many times an edge would have been visible to a poll on the
|
|
-- RENDER clock -- where these reads used to live. Measured on UP
|
|
-- rather than B: the fix TAKES B out of the queue, so B never
|
|
-- reaches `pressed` any more and would read as a false zero. UP is
|
|
-- untouched by everything while the battle is parked, and the
|
|
-- question is about step-vs-frame ordering, not about which button.
|
|
local onFrame, onStep = 0, 0
|
|
local innerU = CatchThrow.update
|
|
CatchThrow.update = function(dt)
|
|
if game.input.wasPressed and game.input:wasPressed("up") then
|
|
onFrame = onFrame + 1
|
|
end
|
|
return innerU(dt)
|
|
end
|
|
local innerB0 = CatchThrow.buttons
|
|
CatchThrow.buttons = function(g)
|
|
local q = g and g.input and g.input.pressQueue
|
|
if q then
|
|
for i = 1, #q do if q[i] == "up" then onStep = onStep + 1 end end
|
|
end
|
|
return innerB0(g)
|
|
end
|
|
love.keypressed("up", "up", false)
|
|
U.wait(2)
|
|
love.keyreleased("up", "up")
|
|
U.wait(2)
|
|
CatchThrow.buttons = innerB0
|
|
CatchThrow.update = innerU
|
|
U.log(("D%dX counter-factual: one UP press -- the logic step saw it "
|
|
.. "%d time(s), the RENDER clock %d time(s)%s")
|
|
:format(speed, onStep, onFrame,
|
|
onFrame == 0 and " <-- a frame poll misses it entirely"
|
|
or ""))
|
|
|
|
local seen = 0
|
|
local innerB = CatchThrow.buttons
|
|
CatchThrow.buttons = function(g)
|
|
local q = g and g.input and g.input.pressQueue
|
|
if q and #q > 0 then
|
|
seen = seen + 1
|
|
U.log("D probe: buttons saw queue [" .. table.concat(q, ",") .. "]")
|
|
end
|
|
return innerB(g)
|
|
end
|
|
love.keypressed(B_KEY, B_KEY, false)
|
|
U.log(("D probe: right after keypressed -- queue=[%s] state.b=%s")
|
|
:format(table.concat(game.input.pressQueue, ","),
|
|
tostring(game.input.state.b)))
|
|
U.wait(2)
|
|
love.keyreleased(B_KEY, B_KEY)
|
|
U.wait(2)
|
|
CatchThrow.buttons = innerB
|
|
U.log("D probe: buttons saw a non-empty queue " .. seen .. " time(s)")
|
|
local ran = false
|
|
for _ = 1, 60 do
|
|
U.wait(2)
|
|
if d.result then ran = true break end
|
|
end
|
|
U.log(("D%dX: after a REAL B -- result=%s (want run) %s")
|
|
:format(speed, tostring(d.result),
|
|
ran and "RAN" or "*** B DID NOTHING ***"))
|
|
else
|
|
U.log("D" .. speed .. ": no armed session to test")
|
|
end
|
|
-- escape hatch: a B that did nothing leaves the battle parked in the
|
|
-- capture phase forever, and the rest of the run must still report
|
|
if not d.result then
|
|
pcall(CatchThrow.onBattleEnded)
|
|
d.result, d.phase, d.afterQueue = "run", "messages", "finish"
|
|
end
|
|
closeOut(d)
|
|
game.speedOverride = nil
|
|
end
|
|
U.log("done -- " .. DIR)
|
|
end
|