mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
CLOSES #806, CLOSES #809, CLOSES #853, CLOSES #854, CLOSES #860, CLOSES #862, CLOSES #865, CLOSES #866
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
-- A trainer's walk-up must stop short of a Strength boulder, and the boulder
|
||||
-- must still be pushable afterwards (#809). TrainerWalkUpToPlayer (pokered
|
||||
-- engine/overworld/trainer_sight.asm) writes dist-1 movement bytes that skip
|
||||
-- collision, so the trainer used to park ON the boulder, and after that
|
||||
-- IsSpriteInFrontOfPlayer (home/overworld.asm) handed TryPushingBoulder the
|
||||
-- trainer instead of the rock. POKEPORT_DRIVER=tests/drivers/boulder_trainer_bug809_test.lua POKEPORT_IDENTITY=bug809 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
|
||||
|
||||
-- No POKEPORT_SPEED: the sighting, the "!" bubble and the walk-up all run at
|
||||
-- the normal 60 Hz logic clock so the stop-short frame is the one a player
|
||||
-- would see. The setup pushes are slow for the same reason; the run takes
|
||||
-- about half a minute of real time before it hands the pad over.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
|
||||
-- pokered data/maps/objects/VictoryRoad3F.asm:
|
||||
-- object_event 13, 3, SPRITE_COOLTRAINER_F, STAY, RIGHT, ..., OPP_COOLTRAINER_F, 3
|
||||
-- object_event 22, 3, SPRITE_BOULDER, STAY, BOULDER_MOVEMENT_BYTE_2, ...
|
||||
-- Her header range is 4 (data/generated/trainer_headers.lua VictoryRoad3F[4]),
|
||||
-- so she spots the player anywhere on row 3 within four cells to her east and
|
||||
-- then walks dist-1 cells toward him. Row 3 is walled at x=19, so BOULDER1
|
||||
-- cannot simply be shoved west into her sight line: it has to go down column
|
||||
-- 22 to row 6, west along row 6, and back up column 17 onto row 3.
|
||||
local MAP = "VICTORY_ROAD_3F"
|
||||
local MAP_LABEL = "VictoryRoad3F"
|
||||
local BOULDER = "VICTORYROAD3F_BOULDER1"
|
||||
local TRAINER = "VICTORYROAD3F_COOLTRAINER_F2"
|
||||
local START = { x = 22, y = 2, facing = "down" }
|
||||
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local pass = true
|
||||
local function check(label, ok)
|
||||
if not ok then pass = false end
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
local function findNpc(ow, name)
|
||||
for _, n in ipairs(ow.npcs or {}) do
|
||||
if n.def and n.def.name == name then return n end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hold `btn` until `cond` goes true or the budget runs out, then release and
|
||||
-- let any half-finished step land. Boulder pushes need a held direction:
|
||||
-- handleInput only reaches checkBoulderPush while the player already faces
|
||||
-- that way, and TryPushingBoulder arms on one poll and moves on the next
|
||||
-- (BIT_TRIED_PUSH_BOULDER), so a single tap can never shift a rock.
|
||||
local function holdUntil(btn, cond, budget)
|
||||
local first = true
|
||||
for _ = 1, budget or 600 do
|
||||
if cond() then break end
|
||||
if first then table.insert(game.input.pressQueue, btn); first = false end
|
||||
game.input.state[btn] = true
|
||||
coroutine.yield()
|
||||
end
|
||||
game.input.state[btn] = false
|
||||
for _ = 1, 40 do
|
||||
if not game.overworld.player.moving and #game.overworld.scriptMoves == 0 then
|
||||
break
|
||||
end
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(4) -- an input-free poll re-arms turning in place (wCheckFor180DegreeTurn)
|
||||
return cond()
|
||||
end
|
||||
|
||||
U.teleport(game, MAP, START.x, START.y, START.facing)
|
||||
U.wait(10)
|
||||
local ow = game.overworld
|
||||
local rock = findNpc(ow, BOULDER)
|
||||
local trainer = findNpc(ow, TRAINER)
|
||||
|
||||
check("BOULDER1 loaded on " .. MAP, rock ~= nil)
|
||||
check("COOLTRAINER_F2 loaded on " .. MAP, trainer ~= nil)
|
||||
if not (rock and trainer) then
|
||||
U.log("map objects missing; nothing to drive")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
check("BOULDER1 starts at the asm cell (22,3)",
|
||||
rock.cellX == 22 and rock.cellY == 3)
|
||||
check("COOLTRAINER_F2 starts at the asm cell (13,3) facing right",
|
||||
trainer.cellX == 13 and trainer.cellY == 3 and trainer.facing == "right")
|
||||
check("checkBoulderPush resolves through pushableAtCell",
|
||||
type(ow.pushableAtCell) == "function")
|
||||
|
||||
local header = game.data:trainerHeader(MAP_LABEL, trainer.def.index)
|
||||
local range = header and header.range or 0
|
||||
check("her sight range is 4 cells", range == 4)
|
||||
|
||||
-- The whole route, so a map or tileset edit shows up here instead of as a
|
||||
-- driver that quietly wanders off. If a cell is not walkable the boulder
|
||||
-- cannot be pushed onto it (CheckForCollisionWhenPushingBoulder reuses the
|
||||
-- player's passability check) and the run is not worth continuing.
|
||||
local ROUTE = {
|
||||
{ 22, 4 }, { 22, 5 }, { 22, 6 }, { 23, 5 }, { 23, 6 },
|
||||
{ 21, 6 }, { 20, 6 }, { 19, 6 }, { 18, 6 }, { 17, 6 },
|
||||
{ 18, 7 }, { 17, 7 }, { 17, 5 }, { 17, 4 }, { 17, 3 },
|
||||
{ 18, 4 }, { 18, 3 }, { 16, 3 }, { 16, 4 }, { 16, 2 },
|
||||
}
|
||||
local routeOk = true
|
||||
for _, c in ipairs(ROUTE) do
|
||||
if not ow.map:isWalkableCell(c[1], c[2]) then
|
||||
routeOk = false
|
||||
U.log("route cell not walkable:", c[1], c[2])
|
||||
end
|
||||
end
|
||||
check("the push route is walkable end to end", routeOk)
|
||||
|
||||
-- STRENGTH is live for the map visit. BIT_STRENGTH_ACTIVE is what
|
||||
-- TryPushingBoulder gates on -- it never re-reads badges or party moves --
|
||||
-- so setting the field-move state is the whole grant (see the comment in
|
||||
-- OverworldState:checkBoulderPush).
|
||||
ow.strengthActive = true
|
||||
|
||||
-- Victory Road rolls a wild encounter on every completed step, not just in
|
||||
-- grass (wild_encounters.asm counts caves as indoor), and this run walks
|
||||
-- twenty-odd cells with an empty party. Drop the map's table: a wild
|
||||
-- battle mid-route interrupts the push with a screen transition and has
|
||||
-- nothing to do with what is being checked.
|
||||
game.data.encounters[MAP] = nil
|
||||
|
||||
if not pass then
|
||||
U.log("setup checks already failed; not driving the push")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
local function boulderAt(x, y)
|
||||
return function() return rock.cellX == x and rock.cellY == y end
|
||||
end
|
||||
local function playerAt(x, y)
|
||||
local p = ow.player
|
||||
return function() return p.cellX == x and p.cellY == y end
|
||||
end
|
||||
|
||||
-- down column 22 to row 6
|
||||
holdUntil("down", boulderAt(22, 6), 400)
|
||||
check("boulder pushed down column 22 to (22,6)", rock.cellX == 22 and rock.cellY == 6)
|
||||
-- around to its east side
|
||||
holdUntil("right", playerAt(23, 5), 120)
|
||||
holdUntil("down", playerAt(23, 6), 120)
|
||||
-- west along row 6 to the column that reaches row 3
|
||||
holdUntil("left", boulderAt(17, 6), 700)
|
||||
check("boulder pushed west along row 6 to (17,6)", rock.cellX == 17 and rock.cellY == 6)
|
||||
-- around to its south side
|
||||
holdUntil("down", playerAt(18, 7), 120)
|
||||
holdUntil("left", playerAt(17, 7), 120)
|
||||
-- up column 17 onto her row
|
||||
holdUntil("up", boulderAt(17, 3), 400)
|
||||
check("boulder pushed up column 17 onto row 3 at (17,3)",
|
||||
rock.cellX == 17 and rock.cellY == 3)
|
||||
if not pass then
|
||||
U.log("the boulder never reached her row; the race below cannot happen")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
-- Step onto row 3 one cell out of range (18 - 13 = 5 > 4) so the sighting
|
||||
-- happens on the push itself and not a moment earlier.
|
||||
holdUntil("right", playerAt(18, 4), 120)
|
||||
holdUntil("up", playerAt(18, 3), 120)
|
||||
check("player waiting at (18,3), one cell outside her range",
|
||||
ow.player.cellX == 18 and ow.player.cellY == 3 and not ow.engaging)
|
||||
|
||||
-- The engage lands on a battle we are not going to fight: stand in for it,
|
||||
-- record where the walk-up stopped, and mark her beaten the way winning
|
||||
-- would. Everything the walk-up does has already happened by this point.
|
||||
local stopped
|
||||
local realEngage = ow.engageTrainer
|
||||
ow.engageTrainer = function(self, npc, onDone)
|
||||
stopped = { npc = npc, x = npc.cellX, y = npc.cellY }
|
||||
game.save.defeatedTrainers[npc.id] = true
|
||||
if onDone then onDone() end
|
||||
end
|
||||
|
||||
-- One push west: the boulder lands on (16,3) and the player follows onto
|
||||
-- (17,3), four cells from her, which is the frame she spots him on.
|
||||
holdUntil("left", function() return stopped ~= nil end, 400)
|
||||
|
||||
check("she spotted the player and finished her walk-up", stopped ~= nil)
|
||||
check("the boulder moved one cell west to (16,3)",
|
||||
rock.cellX == 16 and rock.cellY == 3)
|
||||
if stopped then
|
||||
U.log("she stopped at", stopped.x, stopped.y, "boulder at", rock.cellX, rock.cellY)
|
||||
check("she is not standing on the boulder cell",
|
||||
not (stopped.x == rock.cellX and stopped.y == rock.cellY))
|
||||
check("she stopped one cell short of it, at (15,3)",
|
||||
stopped.x == 15 and stopped.y == 3)
|
||||
check("the push path still finds the boulder under that cell",
|
||||
ow:pushableAtCell(rock.cellX, rock.cellY) == rock)
|
||||
check("nothing else shares the boulder's cell",
|
||||
ow:npcAtCell(rock.cellX, rock.cellY) == rock)
|
||||
end
|
||||
U.shot(game, SHOT_DIR .. "/bug809_walkup_stop.png")
|
||||
|
||||
-- ...and the rock still moves. Push it north, the one free direction left:
|
||||
-- west is her, east is the player, south is where he came from.
|
||||
holdUntil("down", playerAt(17, 4), 120)
|
||||
holdUntil("left", playerAt(16, 4), 120)
|
||||
holdUntil("up", boulderAt(16, 2), 400)
|
||||
if not check("the boulder is still pushable after the engage",
|
||||
rock.cellX == 16 and rock.cellY == 2) then
|
||||
U.log("boulder ended at", rock.cellX, rock.cellY, "player at",
|
||||
ow.player.cellX, ow.player.cellY)
|
||||
end
|
||||
holdUntil("down", playerAt(16, 4), 120)
|
||||
U.shot(game, SHOT_DIR .. "/bug809_still_pushable.png")
|
||||
|
||||
ow.engageTrainer = realEngage
|
||||
U.log(pass and "ALL CHECKS PASSED" or "SOME CHECKS FAILED")
|
||||
|
||||
U.log("On screen: the COOLTRAINER stands at (15,3) with a one-cell gap")
|
||||
U.log("between her and the rock, which now sits at (16,2), one row up from")
|
||||
U.log("where she stopped. The near miss to watch for is her sprite ending")
|
||||
U.log("the walk-up on top of the rock, or standing clear of it but leaving")
|
||||
U.log("it inert: walk into the rock from any side and it should still shift")
|
||||
U.log("a cell. Her battle was stubbed out and she is flagged as beaten;")
|
||||
U.log("re-run the driver to watch the race again from the start.")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
-- Driver: Fighting Dojo prize balls, #853 (dex page first) and #854 (the
|
||||
-- question stays on screen under YES/NO). pokered scripts/FightingDojo.asm
|
||||
-- runs `ld a, HITMONLEE / call DisplayPokedex` before .Text, and .Text is a
|
||||
-- text_end string printed with PrintText immediately followed by YesNoChoice.
|
||||
-- No POKEPORT_SPEED here: the dex page and the YES/NO pop are what is judged.
|
||||
-- SHOT_DIR=/tmp/shots POKEPORT_DRIVER=tests/drivers/dojo_balls_bug853_test.lua POKEPORT_IDENTITY=bug853 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
||||
local MapScripts = require("src.script.MapScripts")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local OW = require("src.world.OverworldController")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
|
||||
-- pokered data/maps/objects/FightingDojo.asm: the two SPRITE_POKE_BALL
|
||||
-- objects sit at (4, 1) HITMONLEE and (5, 1) HITMONCHAN, on the north wall
|
||||
-- under the posters. The only approach is from the mat below them.
|
||||
local MAP = "FIGHTING_DOJO"
|
||||
local LEE = { name = "FIGHTINGDOJO_HITMONLEE_POKE_BALL", x = 4, y = 1 }
|
||||
local CHAN = { name = "FIGHTINGDOJO_HITMONCHAN_POKE_BALL", x = 5, y = 1 }
|
||||
local START = { x = 4, y = 4 } -- walk up from here to (4, 2), facing LEE
|
||||
|
||||
local failures = {}
|
||||
local function check(cond, msg)
|
||||
if cond then U.log("PASS", msg) else
|
||||
failures[#failures + 1] = msg
|
||||
U.log("FAIL", msg)
|
||||
end
|
||||
return cond
|
||||
end
|
||||
|
||||
local function topIs(mt) return getmetatable(game.stack:top()) == mt end
|
||||
local function under()
|
||||
return game.stack.states[#game.stack.states - 1]
|
||||
end
|
||||
|
||||
local function npcByName(ow, name)
|
||||
for _, n in ipairs(ow.npcs or {}) do
|
||||
if n.def and n.def.name == name then return n end
|
||||
end
|
||||
end
|
||||
|
||||
local function pageText()
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) ~= TextBox then return "" end
|
||||
local page = top.pages and top.pages[top.pageIndex]
|
||||
return page and table.concat(page, "\n") or ""
|
||||
end
|
||||
|
||||
local function waitFor(cond, cap)
|
||||
for _ = 1, (cap or 200) do
|
||||
if cond() then return true end
|
||||
U.wait(2)
|
||||
end
|
||||
return cond()
|
||||
end
|
||||
|
||||
local function mashUntil(cond, cap)
|
||||
for _ = 1, (cap or 100) do
|
||||
if cond() then return true end
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
end
|
||||
return cond()
|
||||
end
|
||||
|
||||
-- fresh dojo with the master already beaten and neither prize taken
|
||||
local function seed(x, y, facing)
|
||||
while game.stack:top() do game.stack:pop() end
|
||||
game.save.flags = {
|
||||
EVENT_BEAT_KARATE_MASTER = true,
|
||||
EVENT_BEAT_FIGHTING_DOJO_TRAINER_0 = true,
|
||||
EVENT_BEAT_FIGHTING_DOJO_TRAINER_1 = true,
|
||||
EVENT_BEAT_FIGHTING_DOJO_TRAINER_2 = true,
|
||||
EVENT_BEAT_FIGHTING_DOJO_TRAINER_3 = true,
|
||||
}
|
||||
game.save.defeatedTrainers = { FIGHTING_DOJO_obj_1 = true }
|
||||
game.save.objectToggles = {}
|
||||
game.save.player.name = game.save.player.name or "RED"
|
||||
-- one mon so give_pokemon has a party to append to, and room for a prize
|
||||
game.save.party = { Pokemon.new(game.data, "CHARIZARD", 50) }
|
||||
game.stack:push(OW, MAP, x, y, facing or "up")
|
||||
U.wait(10)
|
||||
return game.stack:top()
|
||||
end
|
||||
|
||||
local ow = seed(START.x, START.y, "up")
|
||||
|
||||
------------------------------------------------------------------
|
||||
-- machine-checkable half: seed, objects, script rows, text, screen id
|
||||
------------------------------------------------------------------
|
||||
check(game.save.flags.EVENT_BEAT_KARATE_MASTER == true,
|
||||
"EVENT_BEAT_KARATE_MASTER is set (the balls answer at all)")
|
||||
check(not game.save.flags.EVENT_GOT_HITMONLEE
|
||||
and not game.save.flags.EVENT_GOT_HITMONCHAN,
|
||||
"neither prize taken yet (no 'greedy' refusal path)")
|
||||
|
||||
local leeBall, chanBall = npcByName(ow, LEE.name), npcByName(ow, CHAN.name)
|
||||
check(leeBall ~= nil, "HITMONLEE ball object loaded")
|
||||
check(chanBall ~= nil, "HITMONCHAN ball object loaded")
|
||||
check(leeBall and leeBall.cellX == LEE.x and leeBall.cellY == LEE.y,
|
||||
("HITMONLEE ball sits at the asm cell (%d, %d)"):format(LEE.x, LEE.y))
|
||||
check(chanBall and chanBall.cellX == CHAN.x and chanBall.cellY == CHAN.y,
|
||||
("HITMONCHAN ball sits at the asm cell (%d, %d)"):format(CHAN.x, CHAN.y))
|
||||
|
||||
check(type(MapScripts.talkScript(MAP, "TEXT_FIGHTINGDOJO_HITMONLEE_POKE_BALL"))
|
||||
== "function",
|
||||
"TEXT_FIGHTINGDOJO_HITMONLEE_POKE_BALL has a hand-ported talk script")
|
||||
check(type(MapScripts.talkScript(MAP, "TEXT_FIGHTINGDOJO_HITMONCHAN_POKE_BALL"))
|
||||
== "function",
|
||||
"TEXT_FIGHTINGDOJO_HITMONCHAN_POKE_BALL has a hand-ported talk script")
|
||||
|
||||
-- the ask() string is the extracted descriptor, not the "You want X?" stub
|
||||
local leeText = game.data.text._FightingDojoHitmonleePokeBallText
|
||||
local chanText = game.data.text._FightingDojoHitmonchanPokeBallText
|
||||
check(type(leeText) == "string" and leeText ~= "",
|
||||
"_FightingDojoHitmonleePokeBallText resolves")
|
||||
check(type(chanText) == "string" and chanText ~= "",
|
||||
"_FightingDojoHitmonchanPokeBallText resolves")
|
||||
if type(leeText) == "string" then
|
||||
U.log("lee prompt reads:", (leeText:gsub("\n", " / ")))
|
||||
end
|
||||
local dexOk = pcall(Screens.get, game, "DexEntryMenu")
|
||||
check(dexOk, "DexEntryMenu resolves through the Screens registry")
|
||||
|
||||
------------------------------------------------------------------
|
||||
-- rehearsal on the HITMONCHAN ball, answered NO so nothing is consumed
|
||||
------------------------------------------------------------------
|
||||
if chanBall then
|
||||
ow:talkTo(chanBall)
|
||||
check(waitFor(function() return topIs(DexEntryMenu) end, 60),
|
||||
"#853: the ball opens the HITMONCHAN dex page before any question")
|
||||
U.shot(game, DIR .. "/dojo_balls_1_dex.png")
|
||||
U.tap(game, "b")
|
||||
check(waitFor(function() return topIs(TextBox) end, 60),
|
||||
"#853: closing the dex page leads into the offer text")
|
||||
mashUntil(function() return topIs(ChoiceBox) end, 60)
|
||||
check(topIs(ChoiceBox), "#854: the YES/NO menu opens on the offer")
|
||||
check(getmetatable(under()) == TextBox,
|
||||
"#854: the question box is still on the stack under the YES/NO menu")
|
||||
U.shot(game, DIR .. "/dojo_balls_2_choice.png")
|
||||
U.tap(game, "b") -- B answers NO; the prize stays unclaimed
|
||||
waitFor(function() return game.stack:top() == ow end, 120)
|
||||
check(not game.save.flags.EVENT_GOT_HITMONCHAN,
|
||||
"answering NO leaves the HITMONCHAN prize unclaimed")
|
||||
check(#game.save.party == 1, "answering NO adds nothing to the party")
|
||||
end
|
||||
|
||||
------------------------------------------------------------------
|
||||
-- hand-off: walk to the HITMONLEE ball and open it for real
|
||||
------------------------------------------------------------------
|
||||
ow = seed(START.x, START.y, "up")
|
||||
for _ = 1, 12 do
|
||||
if ow.player.cellY <= LEE.y + 1 then break end
|
||||
U.hold(game, "up", 16)
|
||||
U.wait(4)
|
||||
end
|
||||
|
||||
local function facingTheBall()
|
||||
local cur = game.overworld
|
||||
local ball = cur and npcByName(cur, LEE.name)
|
||||
if not ball then return false end
|
||||
local fx, fy = cur.player:facingCell()
|
||||
return cur:npcAtCell(fx, fy) == ball
|
||||
end
|
||||
|
||||
if not facingTheBall() then
|
||||
-- a map edit or a mod moved the ball: stand on any free walkable
|
||||
-- neighbour instead. {dx, dy, facing} is the offset from the ball to
|
||||
-- the stand cell plus the direction that looks back at it.
|
||||
local sides = {
|
||||
{ 0, 1, "up" }, { 1, 0, "left" }, { -1, 0, "right" }, { 0, -1, "down" },
|
||||
}
|
||||
for _, s in ipairs(sides) do
|
||||
local cx, cy = LEE.x + s[1], LEE.y + s[2]
|
||||
if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
|
||||
U.log("walk up stopped short; standing on", cx, cy, "facing", s[3])
|
||||
ow = seed(cx, cy, s[3])
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
check(facingTheBall(), "player is standing against the HITMONLEE ball")
|
||||
|
||||
U.tap(game, "a")
|
||||
check(waitFor(function() return topIs(DexEntryMenu) end, 60),
|
||||
"#853: pressing A opens the HITMONLEE dex page")
|
||||
U.shot(game, DIR .. "/dojo_balls_3_handoff.png")
|
||||
|
||||
if #failures == 0 then
|
||||
U.log("all checks passed")
|
||||
else
|
||||
U.log(("%d check(s) failed:"):format(#failures), table.concat(failures, "; "))
|
||||
end
|
||||
|
||||
U.log("On screen now: the HITMONLEE dex page the ball opened, name and")
|
||||
U.log("sprite only, since the mon is seen but not owned yet. Press B: the")
|
||||
U.log("offer types out, and the YES/NO menu should appear above it with the")
|
||||
U.log("question still readable -- the old bug swapped the text away for a")
|
||||
U.log("bare YES/NO over the overworld. Answer YES to take HITMONLEE; the")
|
||||
U.log("HITMONCHAN ball beside it stays put and gives the greedy refusal.")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,9 @@
|
||||
-- BUG1 gate -- the master stops the player on the tile to his left
|
||||
-- BUG2 no speech -- no won text + no prize dialogue after the win
|
||||
-- BUG3 wrong re-talk -- shows the pre-battle challenge, not the after line
|
||||
-- BUG4 (verify) -- the ball ask() is the Gen1 descriptor, not a dex entry
|
||||
-- BUG4 (verify) -- the ball opens the prize's dex preview first
|
||||
-- (FightingDojo.asm DisplayPokedex, #853) and then
|
||||
-- asks with the Gen1 descriptor text
|
||||
-- BUG5 both balls -- the chosen ball AND the other one both vanish; the
|
||||
-- other should stay and give the "greedy" refusal
|
||||
-- BUG6 poster -- the north-wall posters ("Enemies on every side!") are
|
||||
@@ -20,6 +22,7 @@ return function(game)
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
||||
local OW = require("src.world.OverworldController")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Commands = require("src.script.Commands")
|
||||
@@ -35,6 +38,7 @@ return function(game)
|
||||
|
||||
local function topIsTextBox() return getmetatable(game.stack:top()) == TextBox end
|
||||
local function topIsChoice() return getmetatable(game.stack:top()) == ChoiceBox end
|
||||
local function topIsDex() return getmetatable(game.stack:top()) == DexEntryMenu end
|
||||
|
||||
local function currentPageText()
|
||||
local top = game.stack:top()
|
||||
@@ -172,8 +176,14 @@ return function(game)
|
||||
check(leeBall ~= nil and chanBall ~= nil, "BUG5: both prize balls on the mat")
|
||||
if leeBall then
|
||||
ow:talkTo(leeBall)
|
||||
-- DisplayPokedex runs before .Text and YesNoChoice in FightingDojo.asm,
|
||||
-- so the dex page is the first thing the ball opens (#853)
|
||||
U.wait(3)
|
||||
check(topIsDex(), "BUG4: the ball opens the HITMONLEE dex entry first")
|
||||
U.shot(game, DIR .. "/dojo_4_dexentry.png")
|
||||
mashUntil(function() return not topIsDex() end, 20)
|
||||
check(sawText("hard kicking") or sawText("HITMONLEE"),
|
||||
"BUG4: ball asks the Gen1 descriptor prompt (no dex entry)")
|
||||
"BUG4: the dex page is followed by the Gen1 descriptor prompt")
|
||||
U.shot(game, DIR .. "/dojo_4_prompt.png")
|
||||
------------------------------------------------------------------
|
||||
-- BUG5: choose YES -> only the chosen ball vanishes; the other stays
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
-- Driver: #862 Celadon Game Corner poster grunt, loss line + exit walk.
|
||||
-- GameCornerRocketText saves _GameCornerRocketBattleEndText ("Dang!") for
|
||||
-- PrintEndBattleText, and GameCornerRocketBattleScript picks the exit walk
|
||||
-- from the player's cell (pokered/scripts/GameCorner.asm:54-102): east of
|
||||
-- him it is WalkAroundPlayer, DOWN/R/R/UP/R/R/R/R, never UP into the poster.
|
||||
-- No POKEPORT_SPEED: the walk and the battle text are what is under test.
|
||||
-- SHOT_DIR=/tmp/shots POKEPORT_IDENTITY=bug862 POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/game_corner_grunt_bug862_test.lua love .
|
||||
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
os.execute("mkdir -p " .. DIR)
|
||||
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
|
||||
local pass, fail = 0, 0
|
||||
local function check(label, ok, detail)
|
||||
if ok then pass = pass + 1 else fail = fail + 1 end
|
||||
U.log(ok and "PASS" or "FAIL", label, detail or "")
|
||||
return ok
|
||||
end
|
||||
|
||||
-- pokered/data/maps/objects/GameCorner.asm:36 -- the grunt is
|
||||
-- object_event 9, 5, SPRITE_ROCKET, STAY, UP, facing the poster bg_event
|
||||
-- at (9,4), which is wall. Standing east of him on (10,5) is the branch
|
||||
-- that matters: wYCoord ~= 6 and wXCoord ~= 8, so the script takes
|
||||
-- GameCornerMovement_Rocket_WalkAroundPlayer.
|
||||
local MAP = "GAME_CORNER"
|
||||
local NAME = "GAMECORNER_ROCKET"
|
||||
local GX, GY = 9, 5
|
||||
local STAND = { x = 10, y = 5, facing = "left" }
|
||||
local POSTER = { x = 9, y = 4 }
|
||||
-- DOWN, RIGHT, RIGHT, UP, RIGHT x4 from (9,5), ending on (15,5)
|
||||
local AROUND = {
|
||||
{ 9, 6 }, { 10, 6 }, { 11, 6 }, { 11, 5 },
|
||||
{ 12, 5 }, { 13, 5 }, { 14, 5 }, { 15, 5 },
|
||||
}
|
||||
|
||||
-- clean slate: he must not read as already defeated or already hidden
|
||||
game.save.defeatedTrainers = {}
|
||||
game.save.objectToggles = game.save.objectToggles or {}
|
||||
game.save.objectToggles.GAME_CORNER = nil
|
||||
game.save.player = game.save.player or {}
|
||||
game.save.player.name = game.save.player.name or "RED"
|
||||
game.save.money = game.save.money or 3000
|
||||
|
||||
-- a tank that one-shots OPP_ROCKET #7, so the mash win below is quick and
|
||||
-- the same every run whatever the type matchups are
|
||||
local tank = Pokemon.new(game.data, "MEWTWO", 100)
|
||||
tank.moves = {
|
||||
{ id = "PSYCHIC_M", pp = 99 },
|
||||
{ id = "THUNDERBOLT", pp = 99 },
|
||||
{ id = "ICE_BEAM", pp = 99 },
|
||||
{ id = "EARTHQUAKE", pp = 99 },
|
||||
}
|
||||
game.save.party = { tank }
|
||||
|
||||
U.teleport(game, MAP, STAND.x, STAND.y, STAND.facing)
|
||||
local ow = game.overworld
|
||||
|
||||
local function findGrunt()
|
||||
for _, n in ipairs(ow.npcs or {}) do
|
||||
if n.def and n.def.name == NAME then return n end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local grunt = findGrunt()
|
||||
check("GAMECORNER_ROCKET is on the floor", grunt ~= nil)
|
||||
if grunt then
|
||||
check("he stands on (9,5)", grunt.cellX == GX and grunt.cellY == GY,
|
||||
("at (%d,%d)"):format(grunt.cellX, grunt.cellY))
|
||||
end
|
||||
|
||||
-- a map edit or a mod could take (10,5) away; anything east of him keeps
|
||||
-- the WalkAroundPlayer branch, so fall back to a free walkable neighbour
|
||||
-- and say which branch that lands on
|
||||
local function facingGrunt()
|
||||
local g = findGrunt()
|
||||
if not g then return false end
|
||||
local fx, fy = ow.player:facingCell()
|
||||
return ow:npcAtCell(fx, fy) == g
|
||||
end
|
||||
if grunt and not facingGrunt() then
|
||||
local sides = {
|
||||
{ 1, 0, "left" }, { 0, 1, "up" }, { -1, 0, "right" }, { 0, -1, "down" },
|
||||
}
|
||||
for _, s in ipairs(sides) do
|
||||
local cx, cy = grunt.cellX + s[1], grunt.cellY + s[2]
|
||||
if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
|
||||
U.log(("(%d,%d) is blocked; standing on"):format(STAND.x, STAND.y),
|
||||
cx, cy, "facing", s[3])
|
||||
U.teleport(game, MAP, cx, cy, s[3])
|
||||
ow = game.overworld
|
||||
grunt = findGrunt()
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
check("the player is face to face with him", facingGrunt())
|
||||
local px, py = ow.player.cellX, ow.player.cellY
|
||||
local around = not (py == 6 or px == 8)
|
||||
U.log(("talking from (%d,%d): the script should take %s"):format(
|
||||
px, py, around and "WalkAroundPlayer (down, right, right, up, "
|
||||
.. "right x4)" or "WalkDirect (right x5)"))
|
||||
|
||||
-- the two strings the fix depends on, and the poster cell the pre-fix
|
||||
-- single UP step walked him into
|
||||
local t = game.data.text
|
||||
check("_GameCornerRocketBattleEndText resolves",
|
||||
type(t._GameCornerRocketBattleEndText) == "string"
|
||||
and t._GameCornerRocketBattleEndText ~= "",
|
||||
tostring(t._GameCornerRocketBattleEndText))
|
||||
check("_GameCornerRocketAfterBattleText resolves",
|
||||
type(t._GameCornerRocketAfterBattleText) == "string"
|
||||
and t._GameCornerRocketAfterBattleText ~= "")
|
||||
check("(9,4) is the poster wall, not a cell he can stand on",
|
||||
not ow.map:isWalkableCell(POSTER.x, POSTER.y))
|
||||
|
||||
-- engageTrainer has to accept the script-supplied loss line; a stale
|
||||
-- two-parameter copy would silently drop it and print nothing
|
||||
local info = debug.getinfo(ow.engageTrainer, "S")
|
||||
local sigOk = false
|
||||
if info and info.short_src then
|
||||
local src = io.open((info.short_src:gsub("^@", "")), "r")
|
||||
if src then
|
||||
local n = 0
|
||||
for line in src:lines() do
|
||||
n = n + 1
|
||||
if n == info.linedefined then
|
||||
sigOk = line:find("endBattleText", 1, true) ~= nil
|
||||
break
|
||||
end
|
||||
end
|
||||
src:close()
|
||||
end
|
||||
end
|
||||
check("engageTrainer takes an endBattleText argument", sigOk)
|
||||
|
||||
U.shot(game, DIR .. "/bug862_0_before.png")
|
||||
|
||||
-- Talk and mash to a win, recording every battle message in order and
|
||||
-- pausing on the loss line long enough to photograph it.
|
||||
local said, battle = {}, nil
|
||||
local lastSaid, dangShot = nil, false
|
||||
local function sample()
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) == BattleState then
|
||||
battle = battle or top
|
||||
local cur = top.current
|
||||
local text = type(cur) == "table" and cur.text
|
||||
if type(text) == "string" and text ~= lastSaid then
|
||||
lastSaid = text
|
||||
said[#said + 1] = text
|
||||
U.log("battle says:", (text:gsub("\n", " ")))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function pageText()
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) ~= TextBox then return "" end
|
||||
local parts = {}
|
||||
for _, page in ipairs(top.pages or {}) do
|
||||
if type(page) == "table" then
|
||||
for _, line in ipairs(page) do parts[#parts + 1] = tostring(line) end
|
||||
end
|
||||
end
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
local function idle()
|
||||
return game.stack:top() == ow and not ow.runner:isRunning()
|
||||
and #ow.scriptMoves == 0 and not ow.transitioning
|
||||
end
|
||||
|
||||
U.tap(game, "a")
|
||||
local sawAfter = false
|
||||
for f = 1, 4000 do
|
||||
sample()
|
||||
if pageText():find("hideout", 1, true) then sawAfter = true break end
|
||||
local top = game.stack:top()
|
||||
if lastSaid and lastSaid:find("Dang", 1, true) and not dangShot then
|
||||
-- stop mashing for a moment: the loss line is on the battle screen.
|
||||
-- The row is picked up the frame it starts typing, so let it finish
|
||||
-- before the capture or the shot is one letter wide.
|
||||
dangShot = true
|
||||
U.wait(60)
|
||||
U.shot(game, DIR .. "/bug862_1_dang.png")
|
||||
elseif top and top.phase then
|
||||
if top.phase == "menu" then top.menuIndex = 1
|
||||
elseif top.phase == "moveSelect" then top.moveIndex = 1 end
|
||||
U.tap(game, "a")
|
||||
if f > 2400 and top.onFinish then
|
||||
U.log("force-finishing a stalled battle")
|
||||
top.onFinish("win")
|
||||
if game.stack:top() == top then game.stack:pop() end
|
||||
end
|
||||
else
|
||||
U.tap(game, "a")
|
||||
end
|
||||
U.wait(2)
|
||||
sample()
|
||||
end
|
||||
check("reached the after-battle 'hideout' line", sawAfter)
|
||||
check("the battle carried the script's loss line",
|
||||
battle ~= nil and type(battle.endBattleText) == "string"
|
||||
and battle.endBattleText:find("Dang", 1, true) ~= nil,
|
||||
battle and tostring(battle.endBattleText) or "no battle seen")
|
||||
|
||||
-- PrintEndBattleText sits between TrainerDefeatedText and
|
||||
-- MoneyForWinningText (engine/battle/core.asm TrainerBattleVictory)
|
||||
local iDefeat, iDang, iMoney
|
||||
for i, line in ipairs(said) do
|
||||
if not iDefeat and line:find("defeated", 1, true) then iDefeat = i end
|
||||
if not iDang and line:find("Dang", 1, true) then iDang = i end
|
||||
if not iMoney and line:find("winning", 1, true) then iMoney = i end
|
||||
end
|
||||
check("the loss line printed on the battle screen", iDang ~= nil)
|
||||
check("it printed with the ROCKET: name tag",
|
||||
iDang ~= nil and said[iDang]:find(":", 1, true) ~= nil,
|
||||
iDang and said[iDang] or "")
|
||||
check("order is defeated -> Dang! -> payout",
|
||||
iDefeat ~= nil and iDang ~= nil and iMoney ~= nil
|
||||
and iDefeat < iDang and iDang < iMoney,
|
||||
("defeated=%s dang=%s payout=%s"):format(tostring(iDefeat),
|
||||
tostring(iDang),
|
||||
tostring(iMoney)))
|
||||
U.shot(game, DIR .. "/bug862_2_afterbattle.png")
|
||||
|
||||
-- Dismiss the after-battle box and watch the exit walk cell by cell.
|
||||
U.tap(game, "a")
|
||||
local visited, order, lowShot = {}, {}, false
|
||||
local function mark(cx, cy)
|
||||
local key = cx .. "," .. cy
|
||||
if not visited[key] then
|
||||
visited[key] = true
|
||||
order[#order + 1] = key
|
||||
end
|
||||
end
|
||||
-- the last step's hide_object rides its own onDone, so the grunt leaves
|
||||
-- ow.npcs on the frame he lands: count the cell he is walking INTO as
|
||||
-- visited too, or the destination never shows up in the sample
|
||||
local last = { GX, GY }
|
||||
for _ = 1, 900 do
|
||||
local g = findGrunt()
|
||||
if g then
|
||||
mark(g.cellX, g.cellY)
|
||||
last = { g.cellX, g.cellY }
|
||||
if g.targetX and g.targetY then
|
||||
mark(g.targetX, g.targetY)
|
||||
last = { g.targetX, g.targetY }
|
||||
end
|
||||
if g.cellY > GY and not lowShot then
|
||||
lowShot = true
|
||||
U.shot(game, DIR .. "/bug862_3_walk.png")
|
||||
end
|
||||
elseif idle() then
|
||||
break
|
||||
end
|
||||
if game.stack:top() ~= ow then U.tap(game, "a") end
|
||||
U.wait(1)
|
||||
end
|
||||
for _ = 1, 400 do
|
||||
if idle() then break end
|
||||
if game.stack:top() ~= ow then U.tap(game, "a") end
|
||||
U.wait(2)
|
||||
end
|
||||
U.wait(5)
|
||||
U.shot(game, DIR .. "/bug862_4_gone.png")
|
||||
|
||||
U.log("cells he stood on:", table.concat(order, " "))
|
||||
check("he never stood on the poster cell (9,4)",
|
||||
not visited[POSTER.x .. "," .. POSTER.y])
|
||||
check("he never stepped north of his start row", (function()
|
||||
for key in pairs(visited) do
|
||||
local y = tonumber(key:match(",(%d+)$"))
|
||||
if y and y < GY then return false end
|
||||
end
|
||||
return true
|
||||
end)())
|
||||
if around then
|
||||
check("he stepped down to (9,6) to get past the player", visited["9,6"])
|
||||
check("he came back up onto row 5 and finished on (15,5)",
|
||||
last[1] == 15 and last[2] == 5,
|
||||
("last seen on (%d,%d)"):format(last[1], last[2]))
|
||||
else
|
||||
check("he walked straight along row 5 to (15,5)",
|
||||
last[1] == 15 and last[2] == 5 and not visited["9,6"],
|
||||
("last seen on (%d,%d)"):format(last[1], last[2]))
|
||||
end
|
||||
local toggles = game.save.objectToggles.GAME_CORNER
|
||||
check("he despawned only after the last step", findGrunt() == nil)
|
||||
check("his objectToggle is hidden",
|
||||
toggles ~= nil and toggles.GAMECORNER_ROCKET == false)
|
||||
check("he is recorded as defeated",
|
||||
game.save.defeatedTrainers["GAME_CORNER_obj_11"] == true)
|
||||
U.log(("checks: %d passed, %d failed"):format(pass, fail))
|
||||
|
||||
-- Hand the pad over on a clean copy of the same setup so the whole beat
|
||||
-- can be watched at speed.
|
||||
game.save.defeatedTrainers = {}
|
||||
game.save.objectToggles.GAME_CORNER = nil
|
||||
U.teleport(game, MAP, STAND.x, STAND.y, STAND.facing)
|
||||
U.log("You are east of the grunt again, facing him. Press A and win.")
|
||||
U.log("Right looks like: he says his piece on the battle screen after")
|
||||
U.log("\"RED defeated ROCKET!\" -- one box, \"ROCKET: Dang!\" -- and the")
|
||||
U.log("¥ payout comes after it, not before. Then the hideout line, then")
|
||||
U.log("he steps DOWN off row 5, right past you, back up and out east.")
|
||||
U.log("The near miss to watch for: he steps UP into the poster, or the")
|
||||
U.log("Dang! box turns up in the overworld after the battle has torn down.")
|
||||
U.log("Talk to him from (9,6) below instead and he takes the straight")
|
||||
U.log("five-step version east; both are correct, the branch is your cell.")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,177 @@
|
||||
-- Manual check of the Rocket Hideout B4F Jessie & James ambush: James walks
|
||||
-- the full four tiles to the player's side (#865) and their loss line prints
|
||||
-- on the battle screen before the prize money (#866).
|
||||
-- pokeyellow scripts/RocketHideoutB4F.asm (MovementData_45605 falls through
|
||||
-- into _45606) and data/maps/objects/RocketHideoutB4F.asm. No fast-forward:
|
||||
-- POKEPORT_DRIVER=tests/drivers/jessie_james_bug866_test.lua POKEPORT_VERSION=yellow love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Commands = require("src.script.Commands")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local mapScripts = require("data.scripts.init")
|
||||
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local MAP = "ROCKET_HIDEOUT_B4F"
|
||||
local BEAT = "EVENT_BEAT_ROCKET_HIDEOUT_4_JESSIE_JAMES"
|
||||
local JAMES, JESSIE = "ROCKETHIDEOUTB4F_JAMES", "ROCKETHIDEOUTB4F_JESSIE"
|
||||
-- RocketHideoutB4FScript_455a5 fires on wYCoord $e with wXCoord $18 or $19.
|
||||
-- x=24 leaves EVENT_ROCKET_HIDEOUT_4_JESSIE_JAMES_ON_LEFT clear, which is
|
||||
-- the branch that hands the four-step blob to James (object 2, spawned at
|
||||
-- 25,10) and the three-step one to Jessie (object 3, at 24,10).
|
||||
local TRIGGER = { x = 24, y = 14 }
|
||||
local EXPECT = {
|
||||
[JAMES] = { x = 25, y = 14, facing = "left" },
|
||||
[JESSIE] = { x = 24, y = 13, facing = "down" },
|
||||
}
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
check("running the Yellow cache (the duo exists nowhere else)",
|
||||
GameVersion.isYellow())
|
||||
if not GameVersion.isYellow() then
|
||||
U.log("re-run with POKEPORT_VERSION=yellow; nothing below will be true")
|
||||
end
|
||||
|
||||
local hooks = mapScripts.get(MAP)
|
||||
check("yellow_jessie_james registered an onStep for " .. MAP,
|
||||
type(hooks) == "table" and type(hooks.onStep) == "function")
|
||||
check("their talk entries are registered too",
|
||||
type(hooks) == "table" and type(hooks.talk) == "table"
|
||||
and hooks.talk.TEXT_ROCKETHIDEOUTB4F_JAMES ~= nil
|
||||
and hooks.talk.TEXT_ROCKETHIDEOUTB4F_JESSIE ~= nil)
|
||||
|
||||
-- the #866 fix is a new script verb; if a mod shadowed it or the registry
|
||||
-- never picked it up, the row would silently no-op and the line would come
|
||||
-- back after the money instead of before it
|
||||
local verb = Commands.resolve(game.data, "save_end_battle_text")
|
||||
check("save_end_battle_text resolves as a script verb", type(verb) == "function")
|
||||
|
||||
local texts = {}
|
||||
for i = 1, 4 do
|
||||
local key = "_RocketHideoutJessieJamesText" .. i
|
||||
texts[i] = game.data.text[key]
|
||||
check(key .. " resolves to a string",
|
||||
type(texts[i]) == "string" and texts[i] ~= "")
|
||||
end
|
||||
if type(texts[3]) == "string" then
|
||||
U.log("the armed loss line reads:", (texts[3]:gsub("\n", " / ")))
|
||||
end
|
||||
|
||||
local objs = (game.data.maps[MAP] or {}).objects or {}
|
||||
local defs = {}
|
||||
for _, o in ipairs(objs) do
|
||||
if o.name == JAMES or o.name == JESSIE then defs[o.name] = o end
|
||||
end
|
||||
check("James is object 2 of " .. MAP .. ", hidden at (25,10)",
|
||||
defs[JAMES] ~= nil and defs[JAMES].index == 2
|
||||
and defs[JAMES].x == 25 and defs[JAMES].y == 10
|
||||
and defs[JAMES].hidden == true)
|
||||
check("Jessie is object 3, hidden at (24,10)",
|
||||
defs[JESSIE] ~= nil and defs[JESSIE].index == 3
|
||||
and defs[JESSIE].x == 24 and defs[JESSIE].y == 10
|
||||
and defs[JESSIE].hidden == true)
|
||||
|
||||
local rocket = game.data.trainers.OPP_ROCKET
|
||||
check("OPP_ROCKET party 43 (the duo's shared team) exists",
|
||||
rocket ~= nil and rocket.parties ~= nil and rocket.parties[43] ~= nil)
|
||||
|
||||
-- arm the site: the ambush is gated only on its beat flag, so no story
|
||||
-- progress is needed to make it live
|
||||
game.save.flags[BEAT] = nil
|
||||
game.save.flags.EVENT_ROCKET_HIDEOUT_4_JESSIE_JAMES_ON_LEFT = nil
|
||||
check(BEAT .. " cleared, so the trigger is live",
|
||||
game.save.flags[BEAT] == nil)
|
||||
|
||||
-- a real party, because the human has to win the battle for the loss line
|
||||
-- to print at all
|
||||
game.save.party = {
|
||||
Pokemon.new(game.data, "CHARIZARD", 60),
|
||||
Pokemon.new(game.data, "NIDOKING", 58),
|
||||
Pokemon.new(game.data, "STARMIE", 58),
|
||||
}
|
||||
game.save.player.name = "RED"
|
||||
|
||||
-- walk in from the north; the two elevator warps sit on row 15, so the
|
||||
-- approach cannot come from below
|
||||
U.teleport(game, MAP, TRIGGER.x, TRIGGER.y - 1, "down")
|
||||
local ow = game.overworld
|
||||
if not ow.map:isWalkableCell(TRIGGER.x, TRIGGER.y - 1) then
|
||||
-- a map edit moved the free cell: any walkable neighbour of the trigger
|
||||
-- works, the script only reads the tile the player lands on
|
||||
local sides = { { 0, -1, "down" }, { -1, 0, "right" }, { 1, 0, "left" } }
|
||||
for _, s in ipairs(sides) do
|
||||
local cx, cy = TRIGGER.x + s[1], TRIGGER.y + s[2]
|
||||
if ow.map:isWalkableCell(cx, cy) then
|
||||
U.log("standing on", cx, cy, "facing", s[3], "instead")
|
||||
U.teleport(game, MAP, cx, cy, s[3])
|
||||
ow = game.overworld
|
||||
U.hold(game, s[3] == "down" and "down" or (s[3] == "right" and "right" or "left"), 20)
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
U.hold(game, "down", 20)
|
||||
end
|
||||
U.wait(10)
|
||||
check("player stepped onto the trigger tile (24,14)",
|
||||
ow.player.cellX == TRIGGER.x and ow.player.cellY == TRIGGER.y)
|
||||
check("the ambush script is running", ow.runner:isRunning())
|
||||
|
||||
U.log("The cutscene is yours now: press A to read, then fight and win.")
|
||||
U.log("Right looks like both Rockets closing in -- Jessie stopping one tile")
|
||||
U.log("above you, James coming all the way down to stand at your right -- and")
|
||||
U.log("after you win, \"ROCKET: Such a dreadful twerp!\" appearing on the")
|
||||
U.log("battle screen just before the money line. The near-miss to watch for")
|
||||
U.log("is James halting three tiles up by the wall, or that line showing up")
|
||||
U.log("in the overworld box after the payout with no ROCKET: tag on it.")
|
||||
U.log("Two more checks print below as you get to them.")
|
||||
|
||||
local function npcNamed(name)
|
||||
for _, n in ipairs(game.overworld and game.overworld.npcs or {}) do
|
||||
if n.def and n.def.name == name then return n end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local approachDone, battleSeen = false, false
|
||||
while true do
|
||||
if not approachDone then
|
||||
local j, s = npcNamed(JAMES), npcNamed(JESSIE)
|
||||
local ow2 = game.overworld
|
||||
if j and s and not j.moving and not s.moving and ow2
|
||||
and #(ow2.scriptMoves or {}) == 0
|
||||
and (j.cellY > 10 or s.cellY > 10) then
|
||||
approachDone = true
|
||||
check("James walked the full four tiles to (25,14) facing left",
|
||||
j.cellX == EXPECT[JAMES].x and j.cellY == EXPECT[JAMES].y
|
||||
and j.facing == EXPECT[JAMES].facing)
|
||||
check("Jessie stopped three down at (24,13) facing the player",
|
||||
s.cellX == EXPECT[JESSIE].x and s.cellY == EXPECT[JESSIE].y
|
||||
and s.facing == EXPECT[JESSIE].facing)
|
||||
U.log("James at", j.cellX, j.cellY, j.facing,
|
||||
"Jessie at", s.cellX, s.cellY, s.facing)
|
||||
U.shot(game, SHOT_DIR .. "/jj866_approach.png")
|
||||
end
|
||||
end
|
||||
if not battleSeen then
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) == BattleState then
|
||||
battleSeen = true
|
||||
-- BattleState prints endBattleText between _TrainerDefeatedText and
|
||||
-- _MoneyForWinningText, so an armed field IS the ordering fix
|
||||
check("the battle carries the loss line as its end-battle text",
|
||||
type(top.endBattleText) == "string" and top.endBattleText ~= ""
|
||||
and top.endBattleText == texts[3])
|
||||
if type(top.endBattleText) == "string" then
|
||||
U.log("armed:", (top.endBattleText:gsub("\n", " / ")))
|
||||
end
|
||||
end
|
||||
end
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user