mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:01:09 +02:00
grass sway during battles, fix south screen culling
This commit is contained in:
@@ -153,6 +153,11 @@ T.check(fullIds["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
||||
-- and AA, for the opposite reason: it is not a knob on the look at all, it is
|
||||
-- what the look COSTS, and only the player knows what their machine can carry
|
||||
T.check(fullIds["DRAMATIC_SHAPE:aa"], "and AA, which FULL neither sets nor owns")
|
||||
-- SHADOWS on the same reasoning: the sun's pass is what the look costs, and
|
||||
-- a preset for the diorama does not get to spend a second draw of the whole
|
||||
-- world on a machine that cannot carry one
|
||||
T.check(fullIds["DRAMATIC_SHAPE:shadows"],
|
||||
"and SHADOWS, the other row that is a question about the hardware")
|
||||
-- VR survives FULL on AA's reasoning: whether a headset is on the desk is
|
||||
-- not the diorama's to decide
|
||||
T.check(fullIds["DRAMATIC_SHAPE:vr"], "and VR, likewise the hardware's question")
|
||||
@@ -406,14 +411,14 @@ local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
-- (nothing to store, nothing for the mod manager to persist) and is offered
|
||||
-- on every platform, saying WHERE? rather than IMPORT where there is no file
|
||||
-- dialog to open
|
||||
T.eq(#hookedRows, 13, "the options hook added a row per setting, plus the "
|
||||
T.eq(#hookedRows, 14, "the options hook added a row per setting, plus the "
|
||||
.. "STADIUM ROM action row")
|
||||
local grid, curve, water = hookedRows[2], hookedRows[3], hookedRows[5]
|
||||
local battles, backRow, daytime = hookedRows[7], hookedRows[8], hookedRows[10]
|
||||
-- the RENDER DIST row is hookedRows[4], FOREST FX hookedRows[6], LET'S GO
|
||||
-- hookedRows[9] and AA hookedRows[11]; all four are read where they are
|
||||
-- used rather than named here, because this chunk is one main function and
|
||||
-- has 200 local slots to spend
|
||||
-- hookedRows[9], SHADOWS hookedRows[11] and AA hookedRows[12]; all five are
|
||||
-- read where they are used rather than named here, because this chunk is one
|
||||
-- main function and has 200 local slots to spend
|
||||
T.eq(hookedRows[4].label, "RENDER DIST", "the viewport row carries its label")
|
||||
T.eq(hookedRows[4].value(), "FIT",
|
||||
"and defaults to FIT -- the cut IS the window the flat game already "
|
||||
@@ -527,7 +532,7 @@ do
|
||||
local AntiAlias = run.loader.exports.DRAMATIC_SHAPE.lib.require("AntiAlias")
|
||||
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||
local aaGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||
local aa = hookedRows[11]
|
||||
local aa = hookedRows[12]
|
||||
T.eq(aa.label, "AA", "the anti-aliasing row carries its label")
|
||||
T.eq(aa.value(), "OFF",
|
||||
"and starts off -- supersampling is a cost knob, and a mod must not spend "
|
||||
@@ -567,6 +572,49 @@ AntiAlias.expand(320, 200)
|
||||
T.eq(AntiAlias.factor(), 1, "leaving nothing behind for the next pass")
|
||||
end
|
||||
|
||||
-- ------- SHADOWS: the sun's own pass, and the row that stands it down
|
||||
--
|
||||
-- The most expensive thing in the frame after the geometry -- the whole world
|
||||
-- drawn a second time from the light, every time anything in it moves -- so
|
||||
-- the row exists for the machine that cannot carry it. What it must do is
|
||||
-- switch shadows OFF rather than down: both halves of ShadowMap (can the pass
|
||||
-- run, and is there a map to read) hang off the one gate, because the map
|
||||
-- from the last frame is still in the canvas and a row that only stopped the
|
||||
-- pass would leave every shadow frozen in the pose it was switched off in.
|
||||
--
|
||||
-- In an anonymous scope, like every section that wants more than a name or
|
||||
-- two: this file is one chunk and a chunk has 200 local slots, which the
|
||||
-- main function is already sitting on.
|
||||
;(function()
|
||||
local Shadows = run.loader.exports.DRAMATIC_SHAPE.lib.require("Shadows")
|
||||
local ShadowMap = run.loader.exports.DRAMATIC_SHAPE.lib.require("ShadowMap")
|
||||
local shadowRow = hookedRows[11]
|
||||
local shGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||
T.eq(shadowRow.label, "SHADOWS", "the shadow row carries its label")
|
||||
T.eq(shadowRow.value(), "ON",
|
||||
"and starts ON -- cast shadows are as much of the mode as the geometry, "
|
||||
.. "and the row is for the machine that cannot have them")
|
||||
T.eq(Shadows.enabled(), true, "which is what the pass reads")
|
||||
T.eq(ShadowMap.wanted(), true, "and the gate both halves of the sun pass ask")
|
||||
|
||||
shadowRow.step(shGame, 1)
|
||||
T.eq(shadowRow.value(), "OFF", "stepping the row stands the sun pass down")
|
||||
T.eq(shGame.save.options.modOptions.DRAMATIC_SHAPE.shadows, false,
|
||||
"the choice persists beside the other settings, not over them")
|
||||
T.eq(Shadows.enabled(), false, "the pass sees it")
|
||||
T.eq(ShadowMap.wanted(), false, "through the one gate")
|
||||
T.eq(ShadowMap.available(), false,
|
||||
"so the sun pass never opens -- the frame's second draw of the world is "
|
||||
.. "the whole thing being saved")
|
||||
T.eq(ShadowMap.active(), false,
|
||||
"and nothing reads the map either: a pass stopped WITHOUT this would leave "
|
||||
.. "the last frame's shadows standing forever")
|
||||
|
||||
shadowRow.step(shGame, 1)
|
||||
T.eq(shadowRow.value(), "ON", "and the row is a toggle -- one step back on")
|
||||
T.eq(ShadowMap.wanted(), true, "with the sun asked for again")
|
||||
end)()
|
||||
|
||||
-- ------- the animated terrain atlas survives an engine without its seams
|
||||
--
|
||||
-- Regression: cycling palette modes with voxel mode on eventually killed
|
||||
@@ -6187,6 +6235,10 @@ T.check(box.z < 200,
|
||||
.. "the view centre is the bug that cut the top off")
|
||||
do
|
||||
local n, s, x = VB.footprint(math.rad(35), 320, 288, REACH)
|
||||
-- the south edge is the ground the bottom ray lands on PLUS what stands
|
||||
-- there: a tree's feet are south of the row its top is seen on, and the
|
||||
-- cut is by column (see lift)
|
||||
s = s + VB.lift(math.rad(35)) + VB.SOUTH_PAD
|
||||
T.check(math.abs(box.rz - (n + s) * 0.5) < 1e-6,
|
||||
"the half-depth spans the footprint from its south edge to its north")
|
||||
T.check(math.abs(box.z - (200 - (n - s) * 0.5)) < 1e-6,
|
||||
@@ -6196,6 +6248,49 @@ do
|
||||
"both bigger than the window at every tilted rung, so the cut can "
|
||||
.. "never take a bite out of the picture")
|
||||
end
|
||||
|
||||
-- ------- and the geometry standing on the ground it frames
|
||||
--
|
||||
-- The bug: the box was cut to where the frame's rays hit the GROUND, and
|
||||
-- the ground is not what the picture is made of. At every rung past the
|
||||
-- frame's own half-angle the bottom of the screen shows things whose FEET
|
||||
-- are south of that line, and the shader cuts by column -- so a tree one
|
||||
-- pixel outside lost its whole height and the bottom of the frame wore a
|
||||
-- bite out of the scenery, the ground behind showing through.
|
||||
do
|
||||
local tanY = 1 / (2 * VS.FOCAL)
|
||||
T.eq(VB.lift(0), 0,
|
||||
"straight down nothing is lost: a raised point lands NORTH of where its "
|
||||
.. "own ray hits the ground, which no cut can take")
|
||||
T.eq(VB.lift(math.atan(tanY)), 0,
|
||||
"and up to atan(tanY) -- the frame's own half-angle -- it still is")
|
||||
T.check(VB.lift(math.rad(35)) > 0 and VB.lift(math.rad(50)) > VB.lift(math.rad(35))
|
||||
and VB.lift(math.rad(75)) > VB.lift(math.rad(50)),
|
||||
"past it the lift opens with the tilt: the flatter the eye, the more of "
|
||||
.. "a tree is what sits under the bottom of the frame")
|
||||
-- against the ray it is derived from: a point HEIGHT up on the bottom
|
||||
-- edge's own ray, dropped to the ground, is exactly this far south of
|
||||
-- where that ray lands
|
||||
for _, deg in ipairs({ 35, 50, 75 }) do
|
||||
local a = math.rad(deg)
|
||||
local dy = -(math.cos(a) + tanY * math.sin(a))
|
||||
local dz = -(math.sin(a) - tanY * math.cos(a))
|
||||
T.check(math.abs(VB.lift(a) - VB.HEIGHT * (-dz / -dy)) < 1e-9,
|
||||
("%d degrees: the lift is that ray walked back up by HEIGHT")
|
||||
:format(deg))
|
||||
end
|
||||
-- and it is in the CUT, at FIT, which is the rung the artefact was seen on
|
||||
VS.angle = math.rad(35)
|
||||
local fit = VB.frame(0, 0, 320, 288, 3)
|
||||
local n, s = VB.footprint(math.rad(35), 320, 288, REACH)
|
||||
T.check(fit.z + fit.rz >= s + VB.lift(math.rad(35)) - 1e-6,
|
||||
"so the box's south edge clears the feet of everything the bottom of "
|
||||
.. "the frame can show, tree tops included")
|
||||
T.check(fit.z + fit.rz > s and fit.z - fit.rz < -n + 1e-6,
|
||||
"without moving the north edge, which the reach already decides")
|
||||
end
|
||||
VS.angle = math.rad(35)
|
||||
box = VB.frame(100, 200, 320, 288, 3)
|
||||
T.check(box.rx ~= box.rz,
|
||||
"which is the whole reason the box kind carries two half-extents: a "
|
||||
.. "square cut on a wide frame either loses the sides or overshoots")
|
||||
@@ -6337,6 +6432,34 @@ end)()
|
||||
-- the row answers the mode, and OFF answers false
|
||||
T.eq(LetsGo.mode(), false, "LET'S GO defaults to OFF")
|
||||
|
||||
-- ------- the scripted catch tutorials are none of LET'S GO's business
|
||||
--
|
||||
-- The VIRIDIAN CITY old man and Yellow's PROF.OAK / PIKACHU intro are
|
||||
-- the same makeOldManDemo cutscene: the cursor, the bag and the throw
|
||||
-- are all scripted, and nobody keeps the Pokemon. Every rung has to
|
||||
-- leave them exactly as the engine plays them.
|
||||
T.eq(LetsGo.scripted({ demo = true }), true,
|
||||
"the old man's tutorial is a scripted battle")
|
||||
T.eq(LetsGo.scripted({ demo = true, oakDemo = true }), true,
|
||||
"and so is PROF.OAK catching the PIKACHU")
|
||||
T.eq(LetsGo.scripted({ kind = "wild" }), false,
|
||||
"an ordinary wild encounter is not")
|
||||
T.eq(LetsGo.scripted(nil), false, "and no battle at all is not either")
|
||||
do
|
||||
-- the predicates that gate every FULL behaviour, read at FULL
|
||||
local realMode = LetsGo.mode
|
||||
LetsGo.mode = function() return "full" end
|
||||
T.eq(LetsGo.fullWild({ kind = "wild" }), true,
|
||||
"FULL owns an ordinary wild encounter")
|
||||
T.eq(LetsGo.fullWild({ kind = "wild", demo = true }), false,
|
||||
"but never the old man's demo, whatever the row says")
|
||||
T.eq(LetsGo.fullWild({ kind = "wild", oakDemo = true }), false,
|
||||
"nor PROF.OAK's")
|
||||
T.eq(LetsGo.wantsMinigame({ kind = "wild", demo = true }), false,
|
||||
"and a scripted throw is never handed the minigame")
|
||||
LetsGo.mode = realMode
|
||||
end
|
||||
|
||||
-- the ball: a full open-drop-rock-click life without a GPU
|
||||
T.eq(Mat4.rotateZ ~= nil, true, "Mat4 grew the roll the wobble rocks on")
|
||||
do
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
-- 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. "b" is the default binding for the B button.
|
||||
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
|
||||
|
||||
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")
|
||||
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
|
||||
for _ = 1, 40 do U.tap(game, "a"); U.wait(4) end
|
||||
U.wait(60)
|
||||
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
|
||||
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")
|
||||
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)")
|
||||
for _ = 1, 40 do U.tap(game, "a"); U.wait(4) end
|
||||
U.wait(60)
|
||||
|
||||
-- ------- 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
|
||||
key("b")
|
||||
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
|
||||
for _ = 1, 80 do U.tap(game, "a"); U.wait(4) end
|
||||
U.wait(60)
|
||||
game.speedOverride = nil
|
||||
end
|
||||
U.log("done -- " .. DIR)
|
||||
end
|
||||
Reference in New Issue
Block a user