mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
ok the auto driver works but needs work
This commit is contained in:
@@ -12,8 +12,15 @@ function FixedStep:init(callback)
|
|||||||
self.callback = callback
|
self.callback = callback
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
|
||||||
|
-- steps), which silently throttled the high fast-forward levels: at 100X
|
||||||
|
-- a 60fps frame wants ~100 steps. Game:update raises this to fit the
|
||||||
|
-- current speed target; a stall still cannot snowball past one frame's
|
||||||
|
-- intended budget.
|
||||||
|
FixedStep.maxAccum = MAX_ACCUM
|
||||||
|
|
||||||
function FixedStep:update(dt)
|
function FixedStep:update(dt)
|
||||||
self.accum = math.min(self.accum + dt, MAX_ACCUM)
|
self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM)
|
||||||
while self.accum >= self.STEP do
|
while self.accum >= self.STEP do
|
||||||
self.accum = self.accum - self.STEP
|
self.accum = self.accum - self.STEP
|
||||||
self.callback(self.STEP)
|
self.callback(self.STEP)
|
||||||
|
|||||||
+5
-1
@@ -170,7 +170,11 @@ function Game:update(dt)
|
|||||||
-- deferred A and edge pulses land in Input's press queue for this step.
|
-- deferred A and edge pulses land in Input's press queue for this step.
|
||||||
TouchInput:update(dt)
|
TouchInput:update(dt)
|
||||||
-- Fast-forward scales only the logic clock (see src/core/GameSpeed.lua).
|
-- Fast-forward scales only the logic clock (see src/core/GameSpeed.lua).
|
||||||
FixedStep:update(dt * self:logicSpeed())
|
-- Give the accumulator room for one full frame at the current speed,
|
||||||
|
-- or the anti-spiral clamp quietly caps every level above ~15X.
|
||||||
|
local speed = self:logicSpeed()
|
||||||
|
FixedStep.maxAccum = math.max(0.25, speed * FixedStep.STEP * 1.5)
|
||||||
|
FixedStep:update(dt * speed)
|
||||||
-- Audio runs off real time at a fixed 60Hz regardless of game speed or
|
-- Audio runs off real time at a fixed 60Hz regardless of game speed or
|
||||||
-- display refresh, so fades and chip synthesis keep their intended tempo
|
-- display refresh, so fades and chip synthesis keep their intended tempo
|
||||||
-- whether we are at 1X, 10X, or running with vsync disabled.
|
-- whether we are at 1X, 10X, or running with vsync disabled.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ local GameSpeed = {}
|
|||||||
-- attempt is long enough that the iteration loop, not the engine, is the
|
-- attempt is long enough that the iteration loop, not the engine, is the
|
||||||
-- bottleneck. Vsync caps how much a real frame can do, so past 10X the
|
-- bottleneck. Vsync caps how much a real frame can do, so past 10X the
|
||||||
-- multiplier is increasingly a ceiling rather than a rate.
|
-- multiplier is increasingly a ceiling rather than a rate.
|
||||||
GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75 }
|
GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75, 100,500 }
|
||||||
GameSpeed.DEFAULT = 1
|
GameSpeed.DEFAULT = 1
|
||||||
|
|
||||||
function GameSpeed.levelLabel(v)
|
function GameSpeed.levelLabel(v)
|
||||||
|
|||||||
@@ -29,6 +29,46 @@ local function hashSet(list, into)
|
|||||||
return into
|
return into
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Passability of a cell of an UNLOADED map def -- the connected neighbor
|
||||||
|
-- during an edge crossing. pokered's collision check reads the neighbor
|
||||||
|
-- strip's tile bytes, so a step off the map edge onto a solid tile of
|
||||||
|
-- the connected map bumps exactly like an in-map wall; the port needs
|
||||||
|
-- the same read without building the whole Map. Same math as cellTile
|
||||||
|
-- on the raw blocks, honoring the surf rule (water/shore passable only
|
||||||
|
-- while surfing, same fallbacks as Map.new).
|
||||||
|
function Map.defPassable(def, tilesetDef, cx, cy, surfing)
|
||||||
|
if not (def and tilesetDef and tilesetDef.blocks and tilesetDef.walkable) then
|
||||||
|
return true -- no data to judge with: keep the old permissive behavior
|
||||||
|
end
|
||||||
|
local tx, ty = cx * 2, cy * 2 + 1
|
||||||
|
local bx, by = math.floor(tx / 4), math.floor(ty / 4)
|
||||||
|
local id
|
||||||
|
if bx < 0 or by < 0 or bx >= def.width or by >= def.height then
|
||||||
|
id = def.borderBlock
|
||||||
|
else
|
||||||
|
id = def.blocks[by * def.width + bx + 1]
|
||||||
|
end
|
||||||
|
local block = tilesetDef.blocks[(id or 0) + 1]
|
||||||
|
if not block then return false end
|
||||||
|
local tile = block[(ty % 4) * 4 + (tx % 4) + 1]
|
||||||
|
for _, t in ipairs(tilesetDef.walkable) do
|
||||||
|
if t == tile then return true end
|
||||||
|
end
|
||||||
|
if surfing then
|
||||||
|
for _, t in ipairs(tilesetDef.waterTiles or WATER_TILES) do
|
||||||
|
if t == tile then return true end
|
||||||
|
end
|
||||||
|
local shore = tilesetDef.shoreTiles
|
||||||
|
if shore == nil and not NO_SHORE_TILESETS[def.tileset] then
|
||||||
|
shore = SHORE_TILES
|
||||||
|
end
|
||||||
|
for _, t in ipairs(shore or {}) do
|
||||||
|
if t == tile then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function Map.new(def, tilesetDef)
|
function Map.new(def, tilesetDef)
|
||||||
local self = setmetatable({}, Map)
|
local self = setmetatable({}, Map)
|
||||||
self.def = def
|
self.def = def
|
||||||
|
|||||||
@@ -883,8 +883,7 @@ function OverworldState:checkEdgeExit(dir)
|
|||||||
|
|
||||||
local conn = self.map:connection(COMPASS[dir])
|
local conn = self.map:connection(COMPASS[dir])
|
||||||
if conn then
|
if conn then
|
||||||
self:crossConnection(dir, conn)
|
return self:crossConnection(dir, conn)
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -898,7 +897,7 @@ function OverworldState:crossConnection(dir, conn)
|
|||||||
local dest = Game.data.maps[conn.map]
|
local dest = Game.data.maps[conn.map]
|
||||||
if not dest then
|
if not dest then
|
||||||
Logger.warn("connection to unknown map %s", tostring(conn.map))
|
Logger.warn("connection to unknown map %s", tostring(conn.map))
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
local p = self.player
|
local p = self.player
|
||||||
local x, y = p.cellX, p.cellY
|
local x, y = p.cellX, p.cellY
|
||||||
@@ -914,6 +913,15 @@ function OverworldState:crossConnection(dir, conn)
|
|||||||
end
|
end
|
||||||
x = math.max(0, math.min(destW - 1, x))
|
x = math.max(0, math.min(destW - 1, x))
|
||||||
y = math.max(0, math.min(destH - 1, y))
|
y = math.max(0, math.min(destH - 1, y))
|
||||||
|
-- pokered's collision check reads the NEIGHBOR strip's tile bytes, so
|
||||||
|
-- stepping off the edge onto a solid tile of the connected map bumps
|
||||||
|
-- exactly like an in-map wall. Without this read, Pallet's south
|
||||||
|
-- shore (land at x2-3) walked straight onto ROUTE_21 (3,0) -- a
|
||||||
|
-- collision tile -- stranding the player on a cell no walk can leave.
|
||||||
|
if not Map.defPassable(dest, Game.data.tilesets[dest.tileset], x, y,
|
||||||
|
p.surfing) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
self:setMap(conn.map, x, y, p.facing, { seamless = true })
|
self:setMap(conn.map, x, y, p.facing, { seamless = true })
|
||||||
-- place the player one cell before the seam (their old world spot,
|
-- place the player one cell before the seam (their old world spot,
|
||||||
-- which the neighbor strip renders identically) and start the step
|
-- which the neighbor strip renders identically) and start the step
|
||||||
@@ -931,6 +939,7 @@ function OverworldState:crossConnection(dir, conn)
|
|||||||
p.stepFramesCur = Game.save.onBike
|
p.stepFramesCur = Game.save.onBike
|
||||||
and (FieldDefaults.world(Game.data, "bikeStepFrames") or 8)
|
and (FieldDefaults.world(Game.data, "bikeStepFrames") or 8)
|
||||||
or (FieldDefaults.world(Game.data, "stepFrames") or 16)
|
or (FieldDefaults.world(Game.data, "stepFrames") or 16)
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- -------------------------------------------------------------------------
|
-- -------------------------------------------------------------------------
|
||||||
|
|||||||
+353
-30
@@ -1104,6 +1104,9 @@ local mountSurfToward
|
|||||||
-- travelTo); declared here for the same reason as cutToward.
|
-- travelTo); declared here for the same reason as cutToward.
|
||||||
local arriveByWarp
|
local arriveByWarp
|
||||||
|
|
||||||
|
-- pickupBlockingBall (defined below, next to reachableSet) is a GLOBAL,
|
||||||
|
-- like cutToward: the main chunk is at LuaJIT's 200-local ceiling.
|
||||||
|
|
||||||
-- Cross to another region of the same map through a gate. Assigned below
|
-- Cross to another region of the same map through a gate. Assigned below
|
||||||
-- (needs walkOntoWarp); declared here because ops.goto_ calls it.
|
-- (needs walkOntoWarp); declared here because ops.goto_ calls it.
|
||||||
local reEnterThroughGate
|
local reEnterThroughGate
|
||||||
@@ -1141,10 +1144,14 @@ end
|
|||||||
function ops.goto_(s, _where, isLast)
|
function ops.goto_(s, _where, isLast)
|
||||||
local o = ow()
|
local o = ow()
|
||||||
local map = o.map
|
local map = o.map
|
||||||
-- one surf mount per goto: walking ashore dismounts, so an unreachable
|
-- surf mounts per goto, BUDGETED: walking ashore dismounts, so an
|
||||||
-- target beyond a pond can otherwise mount -> cross -> dismount -> fail
|
-- unreachable target beyond a pond can mount -> cross -> dismount ->
|
||||||
-- -> mount again forever (Celadon's pond, target (39,8))
|
-- fail -> mount forever (Celadon's pond, target (39,8)) -- but a hard
|
||||||
local surfMounts = 0
|
-- cap of ONE stranded the bot on Route 20's mid-island strip, where a
|
||||||
|
-- legitimate crossing is surf -> beach -> surf again. Three mounts
|
||||||
|
-- covers every real island-hop; a still-unreachable target then fails
|
||||||
|
-- the goto as before.
|
||||||
|
local surfMounts, SURF_BUDGET = 0, 3
|
||||||
-- ...and a small cut budget per goto: a pocket behind two trees opens
|
-- ...and a small cut budget per goto: a pocket behind two trees opens
|
||||||
-- over two passes, but a target that stays unreachable must not
|
-- over two passes, but a target that stays unreachable must not
|
||||||
-- chain-cut every ornamental bush on the map (Saffron/Celadon cut-spam)
|
-- chain-cut every ornamental bush on the map (Saffron/Celadon cut-spam)
|
||||||
@@ -1171,6 +1178,8 @@ function ops.goto_(s, _where, isLast)
|
|||||||
local tx = math.max(0, math.min(s.x, map.widthCells - 1))
|
local tx = math.max(0, math.min(s.x, map.widthCells - 1))
|
||||||
local ty = math.max(0, math.min(s.y, map.heightCells - 1))
|
local ty = math.max(0, math.min(s.y, map.heightCells - 1))
|
||||||
local refusedEdge = knownWalls(map.id) or {}
|
local refusedEdge = knownWalls(map.id) or {}
|
||||||
|
-- one border-cell retarget per goto (see the fenced-lane rung below)
|
||||||
|
local edgeRetargeted = false
|
||||||
for edgeTry = 1, 2 do
|
for edgeTry = 1, 2 do
|
||||||
for _ = 1, 300 do
|
for _ = 1, 300 do
|
||||||
if ow().map.id ~= startMap then return true end
|
if ow().map.id ~= startMap then return true end
|
||||||
@@ -1206,11 +1215,43 @@ function ops.goto_(s, _where, isLast)
|
|||||||
end
|
end
|
||||||
-- ...or the border cell is on WATER (Pallet's south edge):
|
-- ...or the border cell is on WATER (Pallet's south edge):
|
||||||
-- mount SURF at the shore and re-plan over the sea.
|
-- mount SURF at the shore and re-plan over the sea.
|
||||||
if surfMounts == 0 and mountSurfToward
|
if surfMounts < SURF_BUDGET and mountSurfToward
|
||||||
and mountSurfToward(tx, ty) then
|
and mountSurfToward(tx, ty) then
|
||||||
surfMounts = surfMounts + 1
|
surfMounts = surfMounts + 1
|
||||||
goto edgeStep
|
goto edgeStep
|
||||||
end
|
end
|
||||||
|
-- ...or THIS border column is fenced off while another of
|
||||||
|
-- the same edge is open. Viridian's south border is three
|
||||||
|
-- fenced lanes and only x20-21 connects to the city; the
|
||||||
|
-- planner's seam cell (15,35) is statically walkable-paired
|
||||||
|
-- but dynamically sealed, so the exit spun cutting
|
||||||
|
-- ornamental bushes and surfing the pond. Retarget ONCE to
|
||||||
|
-- the nearest border cell that is BOTH reachable right now
|
||||||
|
-- and has a walkable far side.
|
||||||
|
if not edgeRetargeted then
|
||||||
|
local compass = (dir == "up" and "north")
|
||||||
|
or (dir == "down" and "south")
|
||||||
|
or (dir == "left" and "west") or "east"
|
||||||
|
local conn = map.def.connections and map.def.connections[compass]
|
||||||
|
if conn and seamCells and reachableSet then
|
||||||
|
local seen, WW = reachableSet()
|
||||||
|
local bestAlt, bestD
|
||||||
|
for _, c in ipairs(seamCells(map.def, compass, conn)) do
|
||||||
|
if seen[c[2] * WW + c[1]] and (c[1] ~= tx or c[2] ~= ty) then
|
||||||
|
local d2 = math.abs(c[1] - tx) + math.abs(c[2] - ty)
|
||||||
|
if not bestD or d2 < bestD then bestAlt, bestD = c, d2 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if bestAlt then
|
||||||
|
say(("edge exit %s on %s: border cell (%d,%d) is walled "
|
||||||
|
.. "off; crossing at (%d,%d) instead"):format(dir,
|
||||||
|
tostring(map.id), tx, ty, bestAlt[1], bestAlt[2]))
|
||||||
|
tx, ty = bestAlt[1], bestAlt[2]
|
||||||
|
edgeRetargeted = true
|
||||||
|
goto edgeStep
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
break -- already there, or genuinely no path
|
break -- already there, or genuinely no path
|
||||||
end
|
end
|
||||||
@@ -1250,7 +1291,7 @@ function ops.goto_(s, _where, isLast)
|
|||||||
-- The push stalled. Once per goto: if this edge has a WATER crossing
|
-- The push stalled. Once per goto: if this edge has a WATER crossing
|
||||||
-- and the party can surf, mount and retry the whole exit over the
|
-- and the party can surf, mount and retry the whole exit over the
|
||||||
-- sea column instead of bumping the shore (Cinnabar -> Route 21).
|
-- sea column instead of bumping the shore (Cinnabar -> Route 21).
|
||||||
if edgeTry == 1 and surfMounts == 0 and not ow().player.surfing
|
if edgeTry == 1 and surfMounts < SURF_BUDGET and not ow().player.surfing
|
||||||
and slotKnowing("SURF") then
|
and slotKnowing("SURF") then
|
||||||
local wc = borderWaterCell(map, dir, tx, ty)
|
local wc = borderWaterCell(map, dir, tx, ty)
|
||||||
if wc and mountSurfToward and mountSurfToward(wc[1], wc[2]) then
|
if wc and mountSurfToward and mountSurfToward(wc[1], wc[2]) then
|
||||||
@@ -1353,12 +1394,21 @@ function ops.goto_(s, _where, isLast)
|
|||||||
for _ = 1, 8 do
|
for _ = 1, 8 do
|
||||||
if ow().map.id ~= from then return true end
|
if ow().map.id ~= from then return true end
|
||||||
local m = ow().map
|
local m = ow().map
|
||||||
-- only a warp ON the map border needs the held direction; an
|
-- A warp needing a held press: mirror the engine's
|
||||||
-- interior door fires on its own and has no "outward" to push
|
-- Warp.extraCheck -- facing the map edge, OR a warp-carpet
|
||||||
local outward = (s.x >= m.widthCells - 1 and "right")
|
-- door tile in front (Route 16's gate mats: plain path
|
||||||
or (s.x <= 0 and "left")
|
-- tiles whose warp fires only pressing INTO the building).
|
||||||
or (s.y >= m.heightCells - 1 and "down")
|
-- A true interior door fires on its own; no direction
|
||||||
or (s.y <= 0 and "up")
|
-- passes extraCheck there and we stop pushing.
|
||||||
|
local WarpMod = require("src.world.Warp")
|
||||||
|
local carpets = G.data.field and G.data.field.warpCarpets
|
||||||
|
local outward
|
||||||
|
for _, dd in ipairs(DIRS) do
|
||||||
|
if WarpMod.extraCheck(m, carpets, s.x, s.y, dd[3]) then
|
||||||
|
outward = dd[3]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
if not outward then break end
|
if not outward then break end
|
||||||
pushed = true
|
pushed = true
|
||||||
faceDir(outward)
|
faceDir(outward)
|
||||||
@@ -1464,6 +1514,15 @@ function ops.goto_(s, _where, isLast)
|
|||||||
blocked = 0
|
blocked = 0
|
||||||
goto keepGoing
|
goto keepGoing
|
||||||
end
|
end
|
||||||
|
-- ...or an item ball is the wall (Pokémon Tower 6F's
|
||||||
|
-- RARE_CANDY sits in the floor's only crossing corridor).
|
||||||
|
-- Tried BEFORE arriveByWarp: taking the ball keeps the walk on
|
||||||
|
-- this floor, while arriveByWarp warps clear off it hunting
|
||||||
|
-- the far end.
|
||||||
|
if pickupBlockingBall and pickupBlockingBall(s.x, s.y) then
|
||||||
|
blocked = 0
|
||||||
|
goto keepGoing
|
||||||
|
end
|
||||||
-- ...or the target is in a part of this map no walk connects.
|
-- ...or the target is in a part of this map no walk connects.
|
||||||
--
|
--
|
||||||
-- Cave floors are not one connected room. MT_MOON_B1F is four
|
-- Cave floors are not one connected room. MT_MOON_B1F is four
|
||||||
@@ -1533,7 +1592,7 @@ function ops.goto_(s, _where, isLast)
|
|||||||
-- an NPC-blocked target in a pond town does not trigger a
|
-- an NPC-blocked target in a pond town does not trigger a
|
||||||
-- pointless swim, and ONCE per goto -- coming ashore dismounts,
|
-- pointless swim, and ONCE per goto -- coming ashore dismounts,
|
||||||
-- so a still-unreachable target re-mounts forever otherwise.
|
-- so a still-unreachable target re-mounts forever otherwise.
|
||||||
if surfMounts == 0 and mountSurfToward
|
if surfMounts < SURF_BUDGET and mountSurfToward
|
||||||
and mountSurfToward(s.x, s.y) then
|
and mountSurfToward(s.x, s.y) then
|
||||||
surfMounts = surfMounts + 1
|
surfMounts = surfMounts + 1
|
||||||
blocked = 0
|
blocked = 0
|
||||||
@@ -2439,6 +2498,8 @@ local function walkOntoWarp(wx, wy)
|
|||||||
local toggledSwitch = false
|
local toggledSwitch = false
|
||||||
-- one Victory Road switch solve per call (see solveVictoryRoadSwitches)
|
-- one Victory Road switch solve per call (see solveVictoryRoadSwitches)
|
||||||
local solvedVR = false
|
local solvedVR = false
|
||||||
|
-- one blocking-item-ball pickup per call (see pickupBlockingBall)
|
||||||
|
local tookBall = false
|
||||||
for _ = 1, 300 do
|
for _ = 1, 300 do
|
||||||
if ow().map.id ~= from then return true end
|
if ow().map.id ~= from then return true end
|
||||||
if inBattle() then fightBattle()
|
if inBattle() then fightBattle()
|
||||||
@@ -2456,11 +2517,28 @@ local function walkOntoWarp(wx, wy)
|
|||||||
-- "leave by this edge" idiom -- which is Red shoving downward into
|
-- "leave by this edge" idiom -- which is Red shoving downward into
|
||||||
-- the wall below the door instead of walking out of it.
|
-- the wall below the door instead of walking out of it.
|
||||||
local m = ow().map
|
local m = ow().map
|
||||||
local outward = (wy >= m.heightCells - 1 and "down")
|
-- Which press can actually FIRE this warp? Mirror the engine:
|
||||||
or (wy <= 0 and "up")
|
-- Warp.extraCheck says whether pressing `dir` from this cell can
|
||||||
or (wx <= 0 and "left")
|
-- warp -- facing the map edge, OR a warp-carpet door tile in
|
||||||
or (wx >= m.widthCells - 1 and "right")
|
-- front. Route 16's gate mats are the latter: plain path tiles
|
||||||
or "down"
|
-- whose warp fires only pressing INTO the building ($4B door to
|
||||||
|
-- the LEFT), and the old edge-based guess pushed "down" forever
|
||||||
|
-- ("the step onto (24,10) keeps getting shoved back").
|
||||||
|
local WarpMod = require("src.world.Warp")
|
||||||
|
local carpets = G.data.field and G.data.field.warpCarpets
|
||||||
|
local outward
|
||||||
|
for _, d in ipairs(DIRS) do
|
||||||
|
if WarpMod.extraCheck(m, carpets, wx, wy, d[3]) then
|
||||||
|
outward = d[3]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
outward = outward
|
||||||
|
or (wy >= m.heightCells - 1 and "down")
|
||||||
|
or (wy <= 0 and "up")
|
||||||
|
or (wx <= 0 and "left")
|
||||||
|
or (wx >= m.widthCells - 1 and "right")
|
||||||
|
or "down"
|
||||||
faceDir(outward)
|
faceDir(outward)
|
||||||
walk(outward)
|
walk(outward)
|
||||||
-- If pushing outward does nothing, the warp we are standing on is
|
-- If pushing outward does nothing, the warp we are standing on is
|
||||||
@@ -2543,6 +2621,12 @@ local function walkOntoWarp(wx, wy)
|
|||||||
solvedVR = true
|
solvedVR = true
|
||||||
solveVictoryRoadSwitches()
|
solveVictoryRoadSwitches()
|
||||||
bfsNil = 0
|
bfsNil = 0
|
||||||
|
-- ...or an item ball on the only corridor (Pokémon Tower 6F's
|
||||||
|
-- RARE_CANDY): take it and re-path.
|
||||||
|
elseif not tookBall and pickupBlockingBall
|
||||||
|
and pickupBlockingBall(wx, wy) then
|
||||||
|
tookBall = true
|
||||||
|
bfsNil = 0
|
||||||
else
|
else
|
||||||
bfsNil = (bfsNil or 0) + 1
|
bfsNil = (bfsNil or 0) + 1
|
||||||
if bfsNil >= 8 then
|
if bfsNil >= 8 then
|
||||||
@@ -2638,7 +2722,10 @@ end
|
|||||||
-- pairing is exact and a seam with no walkable pair can never be crossed
|
-- pairing is exact and a seam with no walkable pair can never be crossed
|
||||||
-- however hard we push at it. This is what keeps the search from planning
|
-- however hard we push at it. This is what keeps the search from planning
|
||||||
-- Vermilion -> Saffron -> Cerulean through a walled city.
|
-- Vermilion -> Saffron -> Cerulean through a walled city.
|
||||||
local function seamCells(def, dir, conn)
|
-- GLOBAL (not local): ops.goto_'s edge-exit retarget below needs it and
|
||||||
|
-- is defined earlier in the file; the main chunk is also at LuaJIT's
|
||||||
|
-- 200-local ceiling.
|
||||||
|
function seamCells(def, dir, conn)
|
||||||
local dest = G.data.maps and G.data.maps[conn.map]
|
local dest = G.data.maps and G.data.maps[conn.map]
|
||||||
if not dest then return {} end
|
if not dest then return {} end
|
||||||
local aW, aH = def.width * 2, def.height * 2
|
local aW, aH = def.width * 2, def.height * 2
|
||||||
@@ -2770,14 +2857,16 @@ local function regionGraph()
|
|||||||
local dw = maps[src].warps and maps[src].warps[w.destWarp]
|
local dw = maps[src].warps and maps[src].warps[w.destWarp]
|
||||||
local dr = dw and cellRegionOf(src, dw.x, dw.y)
|
local dr = dw and cellRegionOf(src, dw.x, dw.y)
|
||||||
if sr and dr then
|
if sr and dr then
|
||||||
|
local c = (id:find("^SEAFOAM") or src:find("^SEAFOAM")) and 43 or 3
|
||||||
add(regionNode(id, sr), regionNode(src, dr),
|
add(regionNode(id, sr), regionNode(src, dr),
|
||||||
{ warp = { x = w.x, y = w.y }, cost = 3, dynamic = true })
|
{ warp = { x = w.x, y = w.y }, cost = c, dynamic = true })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif type(d) == "string" and maps[d] then
|
elseif type(d) == "string" and maps[d] then
|
||||||
local dw = maps[d].warps and maps[d].warps[w.destWarp]
|
local dw = maps[d].warps and maps[d].warps[w.destWarp]
|
||||||
local dr = dw and cellRegionOf(d, dw.x, dw.y)
|
local dr = dw and cellRegionOf(d, dw.x, dw.y)
|
||||||
if sr and dr then
|
if sr and dr and not (d:find("^SEAFOAM") and not id:find("^SEAFOAM")) then
|
||||||
|
-- entering Seafoam pruned, same reason as the map graph
|
||||||
local c = (id:find("ELEVATOR") or d:find("ELEVATOR")) and 50 or 1
|
local c = (id:find("ELEVATOR") or d:find("ELEVATOR")) and 50 or 1
|
||||||
add(regionNode(id, sr), regionNode(d, dr),
|
add(regionNode(id, sr), regionNode(d, dr),
|
||||||
{ warp = { x = w.x, y = w.y }, cost = c })
|
{ warp = { x = w.x, y = w.y }, cost = c })
|
||||||
@@ -2802,8 +2891,13 @@ local function regionGraph()
|
|||||||
local k = tostring(sr) .. ">" .. tostring(dr)
|
local k = tostring(sr) .. ">" .. tostring(dr)
|
||||||
if sr and dr and not seen[k] then
|
if sr and dr and not seen[k] then
|
||||||
seen[k] = true
|
seen[k] = true
|
||||||
add(regionNode(id, sr), regionNode(conn.map, dr),
|
-- ROUTE_20 <-> CINNABAR: pruned, same reason as the map
|
||||||
{ dir = COMPASS_DIR[dir], cells = cells, cost = 1 })
|
-- graph (the east and west seas never connect)
|
||||||
|
if not ((id == "ROUTE_20" and conn.map == "CINNABAR_ISLAND")
|
||||||
|
or (id == "CINNABAR_ISLAND" and conn.map == "ROUTE_20")) then
|
||||||
|
add(regionNode(id, sr), regionNode(conn.map, dr),
|
||||||
|
{ dir = COMPASS_DIR[dir], cells = cells, cost = 1 })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2945,7 +3039,17 @@ local function mapGraph()
|
|||||||
-- rather than deleted so a floor only the elevator can reach (Silph's
|
-- rather than deleted so a floor only the elevator can reach (Silph's
|
||||||
-- top) is still on the graph.
|
-- top) is still on the graph.
|
||||||
local elev = (id:find("ELEVATOR") or d:find("ELEVATOR")) and 50 or 1
|
local elev = (id:find("ELEVATOR") or d:find("ELEVATOR")) and 50 or 1
|
||||||
g[id][#g[id] + 1] = { to = d, warp = w, cost = elev }
|
-- ENTERING Seafoam is pruned outright: 1F's two door pockets
|
||||||
|
-- only connect through the BASEMENT floors, which hop execution
|
||||||
|
-- cannot thread, so every plan through the islands ping-ponged
|
||||||
|
-- the Route 20 doors -- pricing alone lost to a wall of banned
|
||||||
|
-- alternatives after hours of wandering. The route never uses
|
||||||
|
-- Seafoam (Cinnabar is reached by sea from Pallet). Exits (the
|
||||||
|
-- LAST_MAP door edges) are kept so a bot stranded inside can
|
||||||
|
-- always leave.
|
||||||
|
if not (d:find("^SEAFOAM") and not id:find("^SEAFOAM")) then
|
||||||
|
g[id][#g[id] + 1] = { to = d, warp = w, cost = elev }
|
||||||
|
end
|
||||||
into[d] = into[d] or {}
|
into[d] = into[d] or {}
|
||||||
into[d][id] = true
|
into[d][id] = true
|
||||||
end
|
end
|
||||||
@@ -2958,8 +3062,23 @@ local function mapGraph()
|
|||||||
-- gate script can open one later, and a wrongly deleted edge is
|
-- gate script can open one later, and a wrongly deleted edge is
|
||||||
-- an unreachable map, while a wrongly costly one is a detour.
|
-- an unreachable map, while a wrongly costly one is a detour.
|
||||||
local cells = seamCells(def, dir, c)
|
local cells = seamCells(def, dir, c)
|
||||||
g[id][#g[id] + 1] = { to = c.map, dir = COMPASS_DIR[dir],
|
-- ROUTE_20's east and west seas NEVER connect on the map: a
|
||||||
cells = cells, cost = #cells > 0 and 1 or 8 }
|
-- walled channel splits them at Seafoam, and the only crossing
|
||||||
|
-- is through the islands' basement, which hop execution cannot
|
||||||
|
-- thread. The map graph sees one ROUTE_20 node, so from the
|
||||||
|
-- east side the CINNABAR seam looked one hop away, and one
|
||||||
|
-- travelTo(PALLET_TOWN) spent HOURS banning its way across
|
||||||
|
-- Kanto until this impossible seam was the cheapest edge left,
|
||||||
|
-- then camped it. PRUNED, not priced -- this is a hard physical
|
||||||
|
-- fact, not a maybe-a-tree-opens-it seam, and both sides stay
|
||||||
|
-- reachable without it (CINNABAR via ROUTE_21, ROUTE_20 via
|
||||||
|
-- ROUTE_19). The executable Cinnabar path is Pallet ->
|
||||||
|
-- ROUTE_21 south, the HOF run's own.
|
||||||
|
if not ((id == "ROUTE_20" and c.map == "CINNABAR_ISLAND")
|
||||||
|
or (id == "CINNABAR_ISLAND" and c.map == "ROUTE_20")) then
|
||||||
|
g[id][#g[id] + 1] = { to = c.map, dir = COMPASS_DIR[dir],
|
||||||
|
cells = cells, cost = #cells > 0 and 1 or 8 }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2972,7 +3091,8 @@ local function mapGraph()
|
|||||||
for _, w in ipairs(def.warps or {}) do
|
for _, w in ipairs(def.warps or {}) do
|
||||||
if w.destMap == "LAST_MAP" then
|
if w.destMap == "LAST_MAP" then
|
||||||
for src in pairs(into[id] or {}) do
|
for src in pairs(into[id] or {}) do
|
||||||
g[id][#g[id] + 1] = { to = src, warp = w, dynamic = true, cost = 3 }
|
local c = (id:find("^SEAFOAM") or src:find("^SEAFOAM")) and 43 or 3
|
||||||
|
g[id][#g[id] + 1] = { to = src, warp = w, dynamic = true, cost = c }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3436,12 +3556,74 @@ function reachableSet()
|
|||||||
and not pairBlockedStep(m, c[1], c[2], nx, ny, p.surfing) then
|
and not pairBlockedStep(m, c[1], c[2], nx, ny, p.surfing) then
|
||||||
seen[id] = true
|
seen[id] = true
|
||||||
q[#q + 1] = { nx, ny }
|
q[#q + 1] = { nx, ny }
|
||||||
|
elseif nx >= 0 and ny >= 0 and nx < W and ny < H
|
||||||
|
and not passableCell(m, nx, ny) then
|
||||||
|
-- ledge hops, so this set matches what bfsNextKey can walk
|
||||||
|
-- (see cutToward for the ROUTE_9 case this fixed)
|
||||||
|
local lx, ly = ledgeLanding(m, c[1], c[2], d[3], d[1], d[2])
|
||||||
|
if lx then
|
||||||
|
local lid = ly * W + lx
|
||||||
|
if not seen[lid] and not blocked[lid] then
|
||||||
|
seen[lid] = true
|
||||||
|
q[#q + 1] = { lx, ly }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return seen, W
|
return seen, W
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Pick up an item ball that is walling off the walk to (tx, ty).
|
||||||
|
--
|
||||||
|
-- An item ball is an NPC and therefore a BFS wall -- and Pokémon Tower
|
||||||
|
-- 6F's RARE_CANDY at (6,8) stands in the ONLY corridor between the
|
||||||
|
-- floor's east and west halves. Vanilla players grab it in passing (the
|
||||||
|
-- ball despawns, opening the way) and the route's own pickup steps do
|
||||||
|
-- the same -- but a resume desync that skips those steps leaves the
|
||||||
|
-- floor severed for good: "goto (6,14) unreachable on POKEMON_TOWER_6F"
|
||||||
|
-- with a fresh save's balls all standing. So: when the target is
|
||||||
|
-- outside the reachable set and a ball sits on its frontier, stand
|
||||||
|
-- beside it, take it, and let the caller re-plan. Same one-per-call
|
||||||
|
-- contract as cutToward.
|
||||||
|
pickingBall = false -- global: the main chunk is at the 200-local ceiling
|
||||||
|
function pickupBlockingBall(tx, ty)
|
||||||
|
if pickingBall then return false end
|
||||||
|
local m = ow().map
|
||||||
|
local seen, W = reachableSet()
|
||||||
|
if seen[ty * W + tx] then return false end -- not a blocking-ball problem
|
||||||
|
for _, npc in ipairs(ow().npcs) do
|
||||||
|
if tostring(npc.def and npc.def.sprite or "") == "SPRITE_POKE_BALL" then
|
||||||
|
local bx, by = npc.cellX, npc.cellY
|
||||||
|
for _, d in ipairs(DIRS) do
|
||||||
|
local ax, ay = bx + d[1], by + d[2]
|
||||||
|
if ax >= 0 and ay >= 0 and ax < W and ay < m.heightCells
|
||||||
|
and seen[ay * W + ax] then
|
||||||
|
pickingBall = true
|
||||||
|
local took = false
|
||||||
|
ops.goto_({ x = ax, y = ay })
|
||||||
|
local p = ow().player
|
||||||
|
if ow().map.id == m.id and p.cellX == ax and p.cellY == ay then
|
||||||
|
faceDir((by < ay and "up") or (by > ay and "down")
|
||||||
|
or (bx < ax and "left") or "right")
|
||||||
|
press("a")
|
||||||
|
U.wait(20)
|
||||||
|
mashUntilIdle()
|
||||||
|
took = not ow():npcAtCell(bx, by)
|
||||||
|
end
|
||||||
|
pickingBall = false
|
||||||
|
if took then
|
||||||
|
say(("picked up the item ball at (%d,%d) that was walling off "
|
||||||
|
.. "(%d,%d)"):format(bx, by, tx, ty))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
-- Reach another region of THIS map by going through a gate and out its far
|
-- Reach another region of THIS map by going through a gate and out its far
|
||||||
-- door.
|
-- door.
|
||||||
--
|
--
|
||||||
@@ -3945,6 +4127,8 @@ end
|
|||||||
-- CASCADEBADGE (src/ui/PartyMenu.lua:342).
|
-- CASCADEBADGE (src/ui/PartyMenu.lua:342).
|
||||||
function ops.fieldMove(s, where)
|
function ops.fieldMove(s, where)
|
||||||
local move = tostring(s.move or ""):upper()
|
local move = tostring(s.move or ""):upper()
|
||||||
|
-- the tree cell CUT will face, for the post-use verification below
|
||||||
|
local cutCellX, cutCellY
|
||||||
local slot = slotKnowing(move)
|
local slot = slotKnowing(move)
|
||||||
if not slot then
|
if not slot then
|
||||||
-- DIG and FLY are TRANSPORT, and a transport step we cannot perform is
|
-- DIG and FLY are TRANSPORT, and a transport step we cannot perform is
|
||||||
@@ -3981,6 +4165,21 @@ function ops.fieldMove(s, where)
|
|||||||
say(("fieldMove %s: nobody in the party knows it"):format(move))
|
say(("fieldMove %s: nobody in the party knows it"):format(move))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
-- The knower must also be CONSCIOUS: the party submenu hides field
|
||||||
|
-- moves on a fainted mon (vanilla -- partyKnows requires hp > 0), so
|
||||||
|
-- the menu walk below finds only STATS/SWITCH and fails. Route 23's
|
||||||
|
-- mandatory surf strip is where this bit: WARTORTLE arrived dead from
|
||||||
|
-- the Viridian Gym attrition, "no SURF entry" x3, the water was never
|
||||||
|
-- crossed, and the whole league skip-cascaded with all eight badges
|
||||||
|
-- in the bag. Heal, fail this attempt loudly, and let the caller's
|
||||||
|
-- retry run the step again with a live user.
|
||||||
|
local knower = (G.save.party or {})[slot]
|
||||||
|
if knower and (knower.hp or 0) <= 0 then
|
||||||
|
say(("fieldMove %s: its only user has fainted -- visiting a centre "
|
||||||
|
.. "before retrying"):format(move))
|
||||||
|
visitPokeCenter(where)
|
||||||
|
return false
|
||||||
|
end
|
||||||
-- CUT acts on the tile the player is FACING (OverworldState:tryCut), and
|
-- CUT acts on the tile the player is FACING (OverworldState:tryCut), and
|
||||||
-- a `goto` leaves us facing whatever direction we last stepped. Without
|
-- a `goto` leaves us facing whatever direction we last stepped. Without
|
||||||
-- turning to the tree first the move fires into empty ground, the game
|
-- turning to the tree first the move fires into empty ground, the game
|
||||||
@@ -3991,6 +4190,19 @@ function ops.fieldMove(s, where)
|
|||||||
local m = ow().map
|
local m = ow().map
|
||||||
local function cuttable(cx, cy)
|
local function cuttable(cx, cy)
|
||||||
if not m:inBounds(cx, cy) or m:isWalkableCell(cx, cy) then return false end
|
if not m:inBounds(cx, cy) or m:isWalkableCell(cx, cy) then return false end
|
||||||
|
-- The same FACE gate as the engine (tryCut): a tree BLOCK is 2x2
|
||||||
|
-- cells and only the tree-tile half is cuttable -- facing the
|
||||||
|
-- fence/sign half answers "Nothing to CUT". Without this the scan
|
||||||
|
-- below picked whichever unwalkable half it met first: Route 9's
|
||||||
|
-- tree at (5,8) has the block's sign half at (4,9), DOWN is
|
||||||
|
-- scanned before RIGHT, and the run cut "the tree" twenty times
|
||||||
|
-- from (4,8) without ever opening the way east.
|
||||||
|
local ts = m.def and m.def.tileset
|
||||||
|
local tile = m:cellTile(cx, cy)
|
||||||
|
if not ((ts == "OVERWORLD" and tile == 0x3d)
|
||||||
|
or (ts == "GYM" and tile == 0x50)) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
local block = m:blockAt(math.floor(cx / 2), math.floor(cy / 2))
|
local block = m:blockAt(math.floor(cx / 2), math.floor(cy / 2))
|
||||||
for _, sw in ipairs(G.data.field.cutTreeSwaps or {}) do
|
for _, sw in ipairs(G.data.field.cutTreeSwaps or {}) do
|
||||||
if sw.before == block then return true end
|
if sw.before == block then return true end
|
||||||
@@ -4001,7 +4213,11 @@ function ops.fieldMove(s, where)
|
|||||||
local facing
|
local facing
|
||||||
for _, d in ipairs({ { 0, 1, "down" }, { 0, -1, "up" },
|
for _, d in ipairs({ { 0, 1, "down" }, { 0, -1, "up" },
|
||||||
{ -1, 0, "left" }, { 1, 0, "right" } }) do
|
{ -1, 0, "left" }, { 1, 0, "right" } }) do
|
||||||
if cuttable(p.cellX + d[1], p.cellY + d[2]) then facing = d[3] break end
|
if cuttable(p.cellX + d[1], p.cellY + d[2]) then
|
||||||
|
facing = d[3]
|
||||||
|
cutCellX, cutCellY = p.cellX + d[1], p.cellY + d[2]
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if not facing then
|
if not facing then
|
||||||
note("fieldMove: no tree adjacent", where)
|
note("fieldMove: no tree adjacent", where)
|
||||||
@@ -4102,6 +4318,19 @@ function ops.fieldMove(s, where)
|
|||||||
press("a")
|
press("a")
|
||||||
U.wait(12)
|
U.wait(12)
|
||||||
mashUntilIdle()
|
mashUntilIdle()
|
||||||
|
-- CUT is verifiable: the faced tree cell must be walkable afterwards.
|
||||||
|
-- "used CUT" used to print regardless, so an engine refusal ("Nothing
|
||||||
|
-- to CUT") looked like success and the caller re-cut the same tree
|
||||||
|
-- for its whole budget.
|
||||||
|
if move == "CUT" and cutCellX then
|
||||||
|
local now = ow().map
|
||||||
|
if now:inBounds(cutCellX, cutCellY)
|
||||||
|
and not now:isWalkableCell(cutCellX, cutCellY) then
|
||||||
|
say(("fieldMove CUT: the tree at (%d,%d) is still standing -- the "
|
||||||
|
.. "cut did not land"):format(cutCellX, cutCellY))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
say(("used %s on %s"):format(move, tostring(where)))
|
say(("used %s on %s"):format(move, tostring(where)))
|
||||||
|
|
||||||
-- DIG and FLY are the route's long-distance travel, and neither lands
|
-- DIG and FLY are the route's long-distance travel, and neither lands
|
||||||
@@ -4182,6 +4411,22 @@ function cutToward(tx, ty)
|
|||||||
and not blockedBy[id] and m:isWalkableCell(nx, ny) then
|
and not blockedBy[id] and m:isWalkableCell(nx, ny) then
|
||||||
seen[id] = true
|
seen[id] = true
|
||||||
queue[#queue + 1] = { nx, ny }
|
queue[#queue + 1] = { nx, ny }
|
||||||
|
elseif nx >= 0 and ny >= 0 and nx < W and ny < H
|
||||||
|
and not m:isWalkableCell(nx, ny) then
|
||||||
|
-- ledge hops reach cells this flood otherwise misses -- and
|
||||||
|
-- bfsNextKey WALKS them, so leaving them out makes the two
|
||||||
|
-- disagree: from ROUTE_9 (46,9) the regrown west tree's face is
|
||||||
|
-- only ledge-reachable, cutToward found "no tree", and
|
||||||
|
-- travelTo(CINNABAR_ISLAND) died -- taking Blaine, the 7-badge
|
||||||
|
-- Viridian Gym and the whole league gate with it.
|
||||||
|
local lx, ly = ledgeLanding(m, c[1], c[2], d[3], d[1], d[2])
|
||||||
|
if lx then
|
||||||
|
local lid = ly * W + lx
|
||||||
|
if not seen[lid] and not blockedBy[lid] then
|
||||||
|
seen[lid] = true
|
||||||
|
queue[#queue + 1] = { lx, ly }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4863,14 +5108,21 @@ function mountSurfToward(tx, ty)
|
|||||||
-- water tilesets only (water_tilesets.asm); a cave with no water would
|
-- water tilesets only (water_tilesets.asm); a cave with no water would
|
||||||
-- scan every cell for nothing
|
-- scan every cell for nothing
|
||||||
local seen, W = reachableSet()
|
local seen, W = reachableSet()
|
||||||
-- nearest reachable land cell with water beside it
|
-- The shore to mount from is the reachable land-beside-water cell
|
||||||
|
-- nearest the TARGET, with the player's own distance only breaking
|
||||||
|
-- ties. Nearest-to-player was a trap on Route 20's mid-island strip:
|
||||||
|
-- a rock wall splits the strip's shores, and the bot re-mounted on
|
||||||
|
-- the west shore every time, surfed back over ground it had already
|
||||||
|
-- crossed, dismounted in the same spot and burned the whole mount
|
||||||
|
-- budget without ever entering the water that leads east.
|
||||||
local best, bestD
|
local best, bestD
|
||||||
for cellId in pairs(seen) do
|
for cellId in pairs(seen) do
|
||||||
local cx, cy = cellId % W, math.floor(cellId / W)
|
local cx, cy = cellId % W, math.floor(cellId / W)
|
||||||
for _, d in ipairs(DIRS) do
|
for _, d in ipairs(DIRS) do
|
||||||
local wx, wy = cx + d[1], cy + d[2]
|
local wx, wy = cx + d[1], cy + d[2]
|
||||||
if m:inBounds(wx, wy) and m:isWaterCell(wx, wy) then
|
if m:inBounds(wx, wy) and m:isWaterCell(wx, wy) then
|
||||||
local dist = math.abs(cx - p.cellX) + math.abs(cy - p.cellY)
|
local dist = (math.abs(cx - tx) + math.abs(cy - ty)) * 4
|
||||||
|
+ math.abs(cx - p.cellX) + math.abs(cy - p.cellY)
|
||||||
if not bestD or dist < bestD then best, bestD = { cx, cy }, dist end
|
if not bestD or dist < bestD then best, bestD = { cx, cy }, dist end
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -6024,6 +6276,26 @@ local function runRoute(startIndex)
|
|||||||
-- already at the heal point, so this is what the death gets attributed to
|
-- already at the heal point, so this is what the death gets attributed to
|
||||||
local lastRanMap
|
local lastRanMap
|
||||||
local diedOn = {} -- segment index -> deaths this attempt
|
local diedOn = {} -- segment index -> deaths this attempt
|
||||||
|
-- Badge go-back (see the league-gate check in the loop): badges whose
|
||||||
|
-- gym segment this attempt has already rewound to, so a gym that
|
||||||
|
-- cannot be won does not loop the rewind forever.
|
||||||
|
local badgeRescued = {}
|
||||||
|
local LEAGUE_GATE_MAPS = {
|
||||||
|
ROUTE_22_GATE = true, ROUTE_23 = true, VICTORY_ROAD_1F = true,
|
||||||
|
VICTORY_ROAD_2F = true, VICTORY_ROAD_3F = true,
|
||||||
|
INDIGO_PLATEAU = true, INDIGO_PLATEAU_LOBBY = true,
|
||||||
|
}
|
||||||
|
-- route order; badge names are inventory keys, gyms are segment maps
|
||||||
|
local BADGE_GYM_SEGMENTS = {
|
||||||
|
{ "BOULDERBADGE", "PEWTER_GYM" },
|
||||||
|
{ "CASCADEBADGE", "CERULEAN_GYM" },
|
||||||
|
{ "THUNDERBADGE", "VERMILION_GYM" },
|
||||||
|
{ "RAINBOWBADGE", "CELADON_GYM" },
|
||||||
|
{ "SOULBADGE", "FUCHSIA_GYM" },
|
||||||
|
{ "MARSHBADGE", "SAFFRON_GYM" },
|
||||||
|
{ "VOLCANOBADGE", "CINNABAR_GYM" },
|
||||||
|
{ "EARTHBADGE", "VIRIDIAN_GYM" },
|
||||||
|
}
|
||||||
-- segment index -> deaths in a row where training gained no levels.
|
-- segment index -> deaths in a row where training gained no levels.
|
||||||
-- Reported, not acted on: no death count writes off a segment any more.
|
-- Reported, not acted on: no death count writes off a segment any more.
|
||||||
-- A long barren streak means the grind cannot reach a training ground
|
-- A long barren streak means the grind cannot reach a training ground
|
||||||
@@ -6291,6 +6563,54 @@ local function runRoute(startIndex)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Rolling checkpoint every 10 segments, no external toucher needed.
|
||||||
|
-- A run once reached segment 182 with all eight badges and then
|
||||||
|
-- "completed" by skipping the sealed league -- and NOTHING was on
|
||||||
|
-- disk: no toucher was running, no abandon fired, and route
|
||||||
|
-- completion never saved. Hours of progress, gone at exit. Never
|
||||||
|
-- again: the driver banks its own progress as it goes.
|
||||||
|
if i % 10 == 0 then
|
||||||
|
saveCheckpoint(i, "rolling (every 10 segments)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- GO BACK FOR A MISSING BADGE before the league gate can wall us.
|
||||||
|
--
|
||||||
|
-- Route 22/23's guards badge-check in order and shove the player
|
||||||
|
-- back (vanilla), and the route only ever moves FORWARD -- so a run
|
||||||
|
-- that lost a gym segment to a travel failure (Blaine's whole
|
||||||
|
-- island skipped over a failed sea trip, once) used to reach the
|
||||||
|
-- gate with seven badges and thrash against the shove until the
|
||||||
|
-- attempt died. The badges are ordinary inventory; when a league
|
||||||
|
-- segment comes up short, rewind the index to the missing badge's
|
||||||
|
-- own gym segment and let the ordinary mismatch->travelTo machinery
|
||||||
|
-- carry us there. Earliest missing badge first (Cinnabar's segment
|
||||||
|
-- sits before Viridian's, and Viridian Gym itself only unlocks at
|
||||||
|
-- seven badges). Once per badge per attempt, so a gym that
|
||||||
|
-- genuinely cannot be won does not loop the rewind forever.
|
||||||
|
if LEAGUE_GATE_MAPS[seg.map] then
|
||||||
|
local missing
|
||||||
|
for _, bg in ipairs(BADGE_GYM_SEGMENTS) do
|
||||||
|
if not (G.save.inventory or {})[bg[1]] and not badgeRescued[bg[1]] then
|
||||||
|
missing = bg
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if missing then
|
||||||
|
local back
|
||||||
|
for j = i - 1, 1, -1 do
|
||||||
|
if ROUTE[j].map == missing[2] then back = j break end
|
||||||
|
end
|
||||||
|
if back then
|
||||||
|
badgeRescued[missing[1]] = true
|
||||||
|
say(("the league gate is ahead but %s is missing -- rewinding "
|
||||||
|
.. "to segment %d/%d (%s) to earn it")
|
||||||
|
:format(missing[1], back, #ROUTE, missing[2]))
|
||||||
|
i = back - 1
|
||||||
|
goto nextSegment
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Standing in an elevator? Ride out to the floor we need, before the
|
-- Standing in an elevator? Ride out to the floor we need, before the
|
||||||
-- map-wait can react to a bounce.
|
-- map-wait can react to a bounce.
|
||||||
--
|
--
|
||||||
@@ -6694,6 +7014,9 @@ local function runRoute(startIndex)
|
|||||||
|
|
||||||
say("---- route complete ----")
|
say("---- route complete ----")
|
||||||
say(("segments: %d failures: %d"):format(#ROUTE, failures))
|
say(("segments: %d failures: %d"):format(#ROUTE, failures))
|
||||||
|
-- bank the final state whatever happened: a "complete" run that
|
||||||
|
-- skip-cascaded its tail used to exit with nothing on disk
|
||||||
|
saveCheckpoint(#ROUTE, "route complete")
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user