mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 17:40:51 +02:00
234 lines
9.7 KiB
Lua
234 lines
9.7 KiB
Lua
-- Driver: choose and photograph one battle arena per map.
|
|
--
|
|
-- data/battle_arenas.lua holds one authored spot per area. This is what
|
|
-- authors it: for every map a battle can happen on, it asks BattleArena for
|
|
-- the arena nearest the map's middle that its clearance test says both mons
|
|
-- can be SEEN in, stages a real battle there, and takes one screenshot.
|
|
--
|
|
-- Two outputs. A `PICK` line per map, ready to paste into the data file, and
|
|
-- a PNG per map to look at -- because the clearance test answers a geometry
|
|
-- question ("nothing tall on the sightline") and the actual question is
|
|
-- whether the picture reads. Grass and flowers around a mon's feet are fine
|
|
-- and wanted; a body cut in half by a wall is not, and only an eye catches
|
|
-- the difference.
|
|
--
|
|
-- SHOT_DIR=.scratchpad/arenas ARENA_FROM=1 ARENA_COUNT=20 \
|
|
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/arena_pick.lua love .
|
|
--
|
|
-- ARENA_FROM / ARENA_COUNT slice the map list so several runs can share the
|
|
-- work; ARENA_MAPS=ID,ID,... does an explicit set instead. ARENA_COUNT=0
|
|
-- lists the maps and stops. ARENA_SURF=1 stages the fight out on the water,
|
|
-- centred in the map's biggest body of it, which is what the surf routes
|
|
-- want. SHOT_DIR must already exist -- the capture writes with io.open,
|
|
-- which does not create directories.
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or ".scratchpad/arenas"
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local BattleState = require("src.battle.BattleState")
|
|
|
|
game.save.party = { Pokemon.new(game.data, "CHARIZARD", 45) }
|
|
game.save.player.name = "RED"
|
|
|
|
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 Arena = lib.require("BattleArena")
|
|
local Battles = lib.require("OverworldBattle")
|
|
|
|
-- ------- staging out on the water
|
|
--
|
|
-- A surf route's land is a rim of beach round the edge of the map, so the
|
|
-- ordinary search always picks the rim and the fight happens on sand at
|
|
-- the corner of a sea. ARENA_SURF=1 says stage this map afloat: water
|
|
-- counts as ground, and the search starts from the middle of the map's
|
|
-- BIGGEST body of water rather than the middle of the map, so the arena
|
|
-- lands out in the open sea instead of against the first shoreline it
|
|
-- finds.
|
|
--
|
|
-- Biggest body, not all water at once: a map with a lake and an ocean has
|
|
-- a centroid between them that is on neither, and the arena would be
|
|
-- pinned to whichever shore that landed nearest.
|
|
local function waterCentre(map)
|
|
local w, h = map.widthCells, map.heightCells
|
|
local seen, best = {}, nil
|
|
for y0 = 0, h - 1 do
|
|
for x0 = 0, w - 1 do
|
|
if not seen[y0 * w + x0] and map:isWaterCell(x0, y0) then
|
|
-- one connected body, flooded from this cell
|
|
local stack, n, sx, sy = { { x0, y0 } }, 0, 0, 0
|
|
seen[y0 * w + x0] = true
|
|
while #stack > 0 do
|
|
local cell = table.remove(stack)
|
|
local cx, cy = cell[1], cell[2]
|
|
n, sx, sy = n + 1, sx + cx, sy + cy
|
|
for _, d in ipairs({ { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }) do
|
|
local nx, ny = cx + d[1], cy + d[2]
|
|
local key = ny * w + nx
|
|
if nx >= 0 and ny >= 0 and nx < w and ny < h and not seen[key]
|
|
and map:isWaterCell(nx, ny) then
|
|
seen[key] = true
|
|
stack[#stack + 1] = { nx, ny }
|
|
end
|
|
end
|
|
end
|
|
if not best or n > best.n then
|
|
best = { n = n, x = sx / n, y = sy / n }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return best
|
|
end
|
|
|
|
-- Which maps a battle can actually happen on: anything with a wild
|
|
-- encounter table or an object that fights. Everything else -- a shop
|
|
-- floor, a stairwell, a bedroom -- would be authoring a spot for a fight
|
|
-- that never happens there.
|
|
--
|
|
-- The encounter tables are their OWN registry keyed by map id, not a field
|
|
-- on the map record. An earlier cut of this read `def.encounters`, which is
|
|
-- always nil -- so the list silently narrowed to trainer maps only and
|
|
-- every route, cave and Safari zone fell out of it. Nothing failed; the
|
|
-- tool just quietly stopped offering to author the places wild battles
|
|
-- actually happen.
|
|
local function fights(id, def)
|
|
local enc = game.data.encounters and game.data.encounters[id]
|
|
if enc then
|
|
for _, kind in ipairs({ "grass", "water" }) do
|
|
local t = enc[kind]
|
|
if t and (t.rate or 0) > 0 and t.slots and t.slots[1] then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
for _, obj in ipairs(def.objects or {}) do
|
|
if obj.trainer or obj.trainerClass or obj.species then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local ids = {}
|
|
local explicit = os.getenv("ARENA_MAPS")
|
|
if explicit and explicit ~= "" then
|
|
for id in explicit:gmatch("[^,%s]+") do ids[#ids + 1] = id end
|
|
else
|
|
for id, def in pairs(game.data.maps) do
|
|
if type(id) == "string" and type(def) == "table" and fights(id, def) then
|
|
ids[#ids + 1] = id
|
|
end
|
|
end
|
|
table.sort(ids)
|
|
end
|
|
|
|
local from = tonumber(os.getenv("ARENA_FROM") or "") or 1
|
|
local count = tonumber(os.getenv("ARENA_COUNT") or "") or #ids
|
|
U.log(("%d battle maps; doing %d..%d"):format(#ids, from,
|
|
math.min(#ids, from + count - 1)))
|
|
-- ARENA_COUNT=0 lists what WOULD be done and stops. The set this filter
|
|
-- picks is the authoritative answer to "which areas need an arena", so it
|
|
-- has to be readable without sitting through a capture -- that is how the
|
|
-- bug above went unnoticed.
|
|
if count <= 0 then
|
|
for i = 1, #ids, 8 do
|
|
U.log("LIST " .. table.concat(ids, ",", i, math.min(#ids, i + 7)))
|
|
end
|
|
U.log("done -- listing only")
|
|
return
|
|
end
|
|
|
|
for i = from, math.min(#ids, from + count - 1) do
|
|
local id = ids[i]
|
|
local def = game.data.maps[id]
|
|
local cx = math.floor(def.width) -- the middle, in cells
|
|
local cy = math.floor(def.height)
|
|
|
|
-- the pick is made against the MAP, before anything is staged, so a map
|
|
-- with no clear arena at all is reported rather than quietly given a bad
|
|
-- one
|
|
local ok, err = pcall(function()
|
|
U.teleport(game, id, 1, 1, "down")
|
|
end)
|
|
if not ok then
|
|
U.log(("SKIP %s -- could not enter (%s)"):format(id, tostring(err)))
|
|
else
|
|
local map = game.overworld.map
|
|
local clear, any
|
|
-- ARENA_AT=x,y[,shape] photographs one specific spot instead of the
|
|
-- picked one, which is how a map the automatic choice got wrong is
|
|
-- re-aimed by hand and checked in the same loop.
|
|
-- ARENA_MAP=OTHER_ID stages on another floor of the same cave or
|
|
-- building, for a map that has nowhere of its own to put a fight
|
|
local at = os.getenv("ARENA_AT")
|
|
local onMap = os.getenv("ARENA_MAP")
|
|
if at and at ~= "" and (explicit and explicit ~= "") then
|
|
local ax, ay, ashape = at:match("^(%-?%d+)%s*,%s*(%-?%d+)%s*,?%s*(%a*)$")
|
|
if ax then
|
|
-- forced through the authored-entry seam, so what gets staged is
|
|
-- exactly what was asked for rather than whatever the search
|
|
-- would have picked from the player's cell
|
|
local onCam = os.getenv("ARENA_CAM")
|
|
Arena.setOverride(id, {
|
|
x = tonumber(ax), y = tonumber(ay),
|
|
shape = (ashape ~= "" and ashape) or "wide",
|
|
map = (onMap ~= "" and onMap) or nil,
|
|
cam = (onCam and onCam ~= "" and onCam) or nil,
|
|
})
|
|
any = Arena.find(map, 0, 0, false)
|
|
clear = any and Arena.clearance(any.map or map, any) or false
|
|
end
|
|
end
|
|
local surf = os.getenv("ARENA_SURF")
|
|
surf = surf ~= nil and surf ~= "" and surf ~= "0"
|
|
if not any then
|
|
local ox, oy = cx, cy
|
|
if surf then
|
|
local sea = waterCentre(map)
|
|
if sea then
|
|
ox, oy = sea.x, sea.y
|
|
U.log(("SEA %s: biggest body is %d cells, centre %.1f,%.1f")
|
|
:format(id, sea.n, sea.x, sea.y))
|
|
else
|
|
U.log("SEA " .. id .. ": no water on this map")
|
|
end
|
|
end
|
|
clear = Arena.search(map, ox, oy, surf, true)
|
|
any = clear or Arena.search(map, ox, oy, surf)
|
|
end
|
|
if not any then
|
|
U.log(("NONE %s -- no arena of either shape"):format(id))
|
|
else
|
|
local onOther = any.map and any.map.id ~= id and any.map.id or nil
|
|
U.log(("PICK [%q] = { %sx = %d, y = %d, shape = %q%s },%s")
|
|
:format(id, onOther and ("map = %q, "):format(onOther) or "",
|
|
any.x, any.y, any.shape,
|
|
any.cam and (", cam = %q"):format(any.cam) or "",
|
|
clear and "" or " -- OBSTRUCTED: no clear spot"))
|
|
-- stand the player on the arena so the staged battle uses it, then
|
|
-- fight something there and photograph the result
|
|
game.overworld.player.cellX = any.playerCell[1]
|
|
game.overworld.player.cellY = any.playerCell[2]
|
|
U.wait(90) -- let the meshes land
|
|
local battle = BattleState.newWild(game, "NIDORINO", 20)
|
|
battle.onFinish = function() end
|
|
game.overworld:pushBattle(battle)
|
|
U.wait(70)
|
|
for _ = 1, 14 do U.tap(game, "a"); U.wait(8) end
|
|
local got = Battles.arena()
|
|
U.log(("SHOT %s at %s,%s"):format(id,
|
|
tostring(got and got.x), tostring(got and got.y)))
|
|
U.shot(game, ("%s/%s.png"):format(DIR, id:lower()))
|
|
while game.stack:top() and game.stack:top() ~= game.overworld do
|
|
game.stack:pop()
|
|
end
|
|
U.wait(6)
|
|
end
|
|
end
|
|
end
|
|
|
|
U.log("done -- " .. DIR)
|
|
end
|