mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 07:00:51 +02:00
update all battle locations
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 1.6.1
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
+88
-64
@@ -23,6 +23,20 @@
|
||||
--
|
||||
-- SHOT_DIR=.scratchpad/arenas \
|
||||
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/arena_pick.lua love .
|
||||
--
|
||||
-- To change ONE map's spot, or to go over these by eye, run the editor
|
||||
-- instead -- it is the same choice made in front of the map rather than in a
|
||||
-- batch. It slides the arena around on a plan of the map with the fit, the
|
||||
-- clearance and the camera's own sightlines answering live, stages a real
|
||||
-- battle on the spot when asked, and writes this file back a map at a time,
|
||||
-- replacing only the lines that changed and leaving every comment here alone:
|
||||
--
|
||||
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/arena_editor.lua lovec .
|
||||
--
|
||||
-- Its export lands in .scratchpad/arena_editor/ to be diffed and copied over;
|
||||
-- ARENA_WRITE=1 points it at this file directly. A comment above an entry it
|
||||
-- moved describes where that spot USED to be, and it says so by name at
|
||||
-- export time -- those are the lines to re-word by hand.
|
||||
|
||||
-- `cam = "wide"` on an entry swaps the long default lens for the 44-degree
|
||||
-- one (BattleCam.RIGS). Both frame the same composition -- the two mons land
|
||||
@@ -49,14 +63,14 @@ return {
|
||||
-- height along the sightline and a hedge in the apron row is not terrain.
|
||||
-- So the choice is where in the clearing, and this is its west end: tree
|
||||
-- line square behind the pair, nothing crossing either of them.
|
||||
["ROUTE_1"] = { x = 4, y = 14, shape = "narrow" },
|
||||
["ROUTE_1"] = { x = 10, y = 16, shape = "narrow", turn = 90 },
|
||||
["ROUTE_2"] = { x = 1, y = 49, shape = "wide" },
|
||||
["ROUTE_3"] = { x = 57, y = 1, shape = "wide" },
|
||||
["ROUTE_4"] = { x = 46, y = 7, shape = "wide" },
|
||||
["ROUTE_5"] = { x = 13, y = 24, shape = "wide" },
|
||||
["ROUTE_6"] = { x = 5, y = 17, shape = "narrow" },
|
||||
["ROUTE_7"] = { x = 8, y = 8, shape = "narrow" },
|
||||
["ROUTE_8"] = { x = 25, y = 7, shape = "wide" },
|
||||
["ROUTE_3"] = { x = 52, y = 10, shape = "wide", turn = 90 },
|
||||
["ROUTE_4"] = { x = 46, y = 4, shape = "wide" },
|
||||
["ROUTE_5"] = { x = 7, y = 0, shape = "wide", turn = 270 },
|
||||
["ROUTE_6"] = { x = 4, y = 15, shape = "narrow" },
|
||||
["ROUTE_7"] = { x = 4, y = 3, shape = "narrow", cam = "wide" },
|
||||
["ROUTE_8"] = { x = 24, y = 5, shape = "wide", turn = 90 },
|
||||
-- the whole route admits six bare wide arenas, all in the west cliff
|
||||
-- corridor; this is the best of them. A flower cluster crosses the far
|
||||
-- mon's hind legs, which the brief allows -- every alternative put a
|
||||
@@ -64,13 +78,13 @@ return {
|
||||
["ROUTE_9"] = { x = 1, y = 11, shape = "wide", cam = "wide" },
|
||||
["ROUTE_10"] = { x = 7, y = 40, shape = "wide" },
|
||||
["ROUTE_11"] = { x = 9, y = 6, shape = "wide" },
|
||||
["ROUTE_12"] = { x = 0, y = 73, shape = "wide" },
|
||||
["ROUTE_13"] = { x = 50, y = 8, shape = "narrow" },
|
||||
["ROUTE_14"] = { x = 11, y = 25, shape = "wide" },
|
||||
["ROUTE_15"] = { x = 9, y = 10, shape = "wide" },
|
||||
["ROUTE_16"] = { x = 6, y = 10, shape = "wide" },
|
||||
["ROUTE_12"] = { x = 10, y = 71, shape = "wide", turn = 90 },
|
||||
["ROUTE_13"] = { x = 51, y = 5, shape = "narrow" },
|
||||
["ROUTE_14"] = { x = 11, y = 24, shape = "wide" },
|
||||
["ROUTE_15"] = { x = 30, y = 10, shape = "wide", turn = 90 },
|
||||
["ROUTE_16"] = { x = 8, y = 10, shape = "wide", turn = 90 },
|
||||
["ROUTE_17"] = { x = 14, y = 70, shape = "wide" },
|
||||
["ROUTE_18"] = { x = 11, y = 4, shape = "wide" },
|
||||
["ROUTE_18"] = { x = 11, y = 3, shape = "wide" },
|
||||
|
||||
-- ------- buildings and caves
|
||||
--
|
||||
@@ -78,10 +92,10 @@ return {
|
||||
-- or gravestones in it rarely holds a 3x6 clearing whose whole width is
|
||||
-- also SEEN, and giving up the apron is usually the difference between a
|
||||
-- fight in the open and one behind a console.
|
||||
["POKEMON_MANSION_1F"] = { x = 4, y = 12, shape = "wide" },
|
||||
["POKEMON_MANSION_2F"] = { x = 15, y = 17, shape = "wide" },
|
||||
["POKEMON_MANSION_3F"] = { x = 23, y = 2, shape = "narrow", cam = "wide" },
|
||||
["POKEMON_MANSION_B1F"] = { x = 19, y = 10, shape = "wide" },
|
||||
["POKEMON_MANSION_1F"] = { x = 4, y = 12, shape = "wide", cam = "wide" },
|
||||
["POKEMON_MANSION_2F"] = { x = 10, y = 1, shape = "wide", cam = "wide" },
|
||||
["POKEMON_MANSION_3F"] = { x = 24, y = 3, shape = "narrow", cam = "wide" },
|
||||
["POKEMON_MANSION_B1F"] = { x = 6, y = 19, shape = "wide", turn = 90 },
|
||||
["POKEMON_TOWER_2F"] = { x = 4, y = 7, shape = "narrow" },
|
||||
["POKEMON_TOWER_3F"] = { x = 4, y = 6, shape = "wide" },
|
||||
-- every one of 4F's thirty candidate spots puts a gravestone through a
|
||||
@@ -89,73 +103,79 @@ return {
|
||||
-- is shot there
|
||||
["POKEMON_TOWER_4F"] = { map = "POKEMON_TOWER_3F", x = 4, y = 6,
|
||||
shape = "wide" },
|
||||
["POKEMON_TOWER_5F"] = { x = 10, y = 1, shape = "narrow" },
|
||||
["POKEMON_TOWER_6F"] = { x = 14, y = 6, shape = "narrow" },
|
||||
["POKEMON_TOWER_5F"] = { x = 8, y = 9, shape = "narrow", cam = "wide" },
|
||||
["POKEMON_TOWER_6F"] = { x = 14, y = 6, shape = "narrow", cam = "wide" },
|
||||
["POKEMON_TOWER_7F"] = { x = 9, y = 5, shape = "wide" },
|
||||
["POWER_PLANT"] = { x = 18, y = 5, shape = "narrow" },
|
||||
["ROCK_TUNNEL_1F"] = { x = 14, y = 15, shape = "wide" },
|
||||
["ROCK_TUNNEL_B1F"] = { x = 20, y = 17, shape = "wide" },
|
||||
["ROCKET_HIDEOUT_B1F"] = { x = 11, y = 6, shape = "narrow" },
|
||||
["ROCKET_HIDEOUT_B2F"] = { x = 19, y = 7, shape = "narrow" },
|
||||
["ROCKET_HIDEOUT_B3F"] = { x = 22, y = 11, shape = "narrow" },
|
||||
["ROCKET_HIDEOUT_B4F"] = { x = 17, y = 3, shape = "narrow" },
|
||||
["ROCKET_HIDEOUT_B1F"] = { x = 20, y = 18, shape = "narrow" },
|
||||
["ROCKET_HIDEOUT_B2F"] = { x = 20, y = 10, shape = "narrow", cam = "wide" },
|
||||
["ROCKET_HIDEOUT_B3F"] = { x = 24, y = 15,
|
||||
shape = "narrow", turn = 90, cam = "wide" },
|
||||
["ROCKET_HIDEOUT_B4F"] = { x = 19, y = 17, shape = "wide", cam = "wide" },
|
||||
["SAFARI_ZONE_CENTER"] = { x = 1, y = 8, shape = "wide" },
|
||||
["SAFARI_ZONE_EAST"] = { x = 21, y = 8, shape = "wide" },
|
||||
["SAFARI_ZONE_NORTH"] = { x = 19, y = 14, shape = "wide" },
|
||||
["SAFARI_ZONE_WEST"] = { x = 18, y = 3, shape = "wide" },
|
||||
["SAFARI_ZONE_EAST"] = { x = 16, y = 8,
|
||||
shape = "wide", turn = 90, cam = "wide" },
|
||||
["SAFARI_ZONE_NORTH"] = { x = 22, y = 12, shape = "wide", turn = 90 },
|
||||
["SAFARI_ZONE_WEST"] = { x = 20, y = 2, shape = "wide" },
|
||||
["SEAFOAM_ISLANDS_1F"] = { x = 14, y = 7, shape = "wide" },
|
||||
["SEAFOAM_ISLANDS_B1F"] = { x = 11, y = 1, shape = "wide" },
|
||||
["SEAFOAM_ISLANDS_B2F"] = { x = 16, y = 2, shape = "wide" },
|
||||
["SEAFOAM_ISLANDS_B3F"] = { x = 25, y = 7, shape = "wide" },
|
||||
["SEAFOAM_ISLANDS_B4F"] = { x = 12, y = 6, shape = "narrow" },
|
||||
["SEAFOAM_ISLANDS_B1F"] = { x = 9, y = 8,
|
||||
shape = "wide", turn = 90, cam = "wide" },
|
||||
["SEAFOAM_ISLANDS_B2F"] = { x = 15, y = 9, shape = "wide", turn = 90 },
|
||||
["SEAFOAM_ISLANDS_B3F"] = { x = 26, y = 7, shape = "wide", turn = 180 },
|
||||
["SEAFOAM_ISLANDS_B4F"] = { x = 9, y = 7, shape = "narrow" },
|
||||
-- Silph Co is office floors partitioned into small rooms, so the long lens
|
||||
-- often lands outside the walls it is meant to be looking between; the
|
||||
-- floors that could not be framed any other way ask for the wide one.
|
||||
["SILPH_CO_2F"] = { x = 16, y = 8, shape = "narrow" },
|
||||
["SILPH_CO_4F"] = { x = 24, y = 2, shape = "narrow" },
|
||||
["SILPH_CO_2F"] = { x = 4, y = 9, shape = "narrow", turn = 270 },
|
||||
["SILPH_CO_4F"] = { x = 14, y = 14, shape = "narrow", turn = 90 },
|
||||
["SILPH_CO_5F"] = { x = 16, y = 7, shape = "wide" },
|
||||
["SILPH_CO_6F"] = { x = 10, y = 8, shape = "narrow" },
|
||||
["SILPH_CO_6F"] = { x = 19, y = 2,
|
||||
shape = "narrow", turn = 90, cam = "wide" },
|
||||
["SILPH_CO_7F"] = { x = 1, y = 2, shape = "wide", cam = "wide" },
|
||||
["SILPH_CO_8F"] = { x = 8, y = 6, shape = "narrow" },
|
||||
["SILPH_CO_9F"] = { x = 20, y = 11, shape = "wide", cam = "wide" },
|
||||
["SS_ANNE_1F_ROOMS"] = { x = 10, y = 1, shape = "narrow", cam = "wide" },
|
||||
["SS_ANNE_1F_ROOMS"] = { x = 11, y = 1, shape = "narrow", cam = "wide" },
|
||||
-- the ship is all two-cell corridors, so the wide arena shape fits nowhere
|
||||
-- aboard and the long lens always lands outside the hull
|
||||
["SS_ANNE_2F"] = { x = 36, y = 8, shape = "narrow", cam = "wide" },
|
||||
-- these two decks are byte-identical geometry, so they take the same spot
|
||||
["SS_ANNE_2F_ROOMS"] = { x = 11, y = 12, shape = "narrow" },
|
||||
["SS_ANNE_B1F_ROOMS"] = { x = 11, y = 12, shape = "narrow" },
|
||||
["SS_ANNE_2F_ROOMS"] = { x = 11, y = 12, shape = "narrow", cam = "wide" },
|
||||
["SS_ANNE_B1F_ROOMS"] = { x = 11, y = 12, shape = "narrow", cam = "wide" },
|
||||
["SS_ANNE_BOW"] = { x = 8, y = 3, shape = "wide" },
|
||||
["VICTORY_ROAD_2F"] = { x = 16, y = 6, shape = "wide" },
|
||||
["VICTORY_ROAD_3F"] = { x = 20, y = 1, shape = "wide" },
|
||||
["VICTORY_ROAD_2F"] = { x = 16, y = 6, shape = "wide", cam = "wide" },
|
||||
["VICTORY_ROAD_3F"] = { x = 20, y = 1, shape = "wide", cam = "wide" },
|
||||
|
||||
-- ------- towns and the last interiors
|
||||
["CERULEAN_CITY"] = { x = 15, y = 16, shape = "wide" },
|
||||
["GAME_CORNER"] = { x = 8, y = 7, shape = "wide" },
|
||||
["GAME_CORNER"] = { x = 8, y = 5, shape = "wide" },
|
||||
-- the lab is ten cells by twelve, so the long lens is always off-map, and
|
||||
-- on it a desk clipped one mon and a pillar the other
|
||||
["OAKS_LAB"] = { x = 3, y = 2, shape = "narrow", cam = "wide" },
|
||||
["OAKS_LAB"] = { x = 1, y = 3, shape = "narrow", turn = 90, cam = "wide" },
|
||||
-- Saffron's gym is a grid of small walled cells: no wide shape exists
|
||||
-- anywhere in it, and the long lens sits inside a divider
|
||||
["SAFFRON_GYM"] = { x = 9, y = 7, shape = "narrow", cam = "wide" },
|
||||
["SILPH_CO_3F"] = { x = 18, y = 11, shape = "wide", cam = "wide" },
|
||||
["SILPH_CO_10F"] = { x = 1, y = 2, shape = "wide" },
|
||||
["SILPH_CO_11F"] = { x = 1, y = 11, shape = "wide", cam = "wide" },
|
||||
["SILPH_CO_10F"] = { x = 1, y = 1, shape = "wide", cam = "wide" },
|
||||
["SILPH_CO_11F"] = { x = 10, y = 6,
|
||||
shape = "wide", turn = 180, cam = "wide" },
|
||||
-- the upper corridor (cols 4-5, rows 1-4) is sealed at runtime by the
|
||||
-- gym's barrier, so arenas there silently fail the fit test
|
||||
["VERMILION_GYM"] = { x = 4, y = 11, shape = "narrow" },
|
||||
["VICTORY_ROAD_1F"] = { x = 11, y = 2, shape = "narrow" },
|
||||
["VERMILION_GYM"] = { x = 3, y = 2,
|
||||
shape = "narrow", turn = 90, cam = "wide" },
|
||||
["VICTORY_ROAD_1F"] = { x = 11, y = 5, shape = "narrow", cam = "wide" },
|
||||
["VIRIDIAN_FOREST"] = { x = 16, y = 34, shape = "narrow" },
|
||||
|
||||
-- ------- the remaining routes
|
||||
["ROUTE_19"] = { x = 8, y = 6, shape = "narrow" },
|
||||
["ROUTE_19"] = { x = 8, y = 31, shape = "narrow" },
|
||||
-- the two surf routes fight AFLOAT, in the middle of their own sea rather
|
||||
-- than on the rim of beach the land search would otherwise find
|
||||
["ROUTE_20"] = { x = 23, y = 7, shape = "wide" },
|
||||
["ROUTE_21"] = { x = 8, y = 46, shape = "wide" },
|
||||
["ROUTE_22"] = { x = 35, y = 7, shape = "wide" },
|
||||
["ROUTE_23"] = { x = 4, y = 36, shape = "wide" },
|
||||
["ROUTE_24"] = { x = 13, y = 15, shape = "wide" },
|
||||
["ROUTE_22"] = { x = 35, y = 7, shape = "wide", cam = "wide" },
|
||||
["ROUTE_23"] = { x = 2, y = 34, shape = "wide", cam = "wide" },
|
||||
["ROUTE_24"] = { x = 6, y = 9, shape = "wide", turn = 270, cam = "wide" },
|
||||
["ROUTE_25"] = { x = 32, y = 2, shape = "wide", cam = "wide" },
|
||||
|
||||
-- ------- caves, gyms and the Elite Four
|
||||
@@ -163,25 +183,29 @@ return {
|
||||
-- None of these tilesets has a grass tile at all, so the no-grass rule
|
||||
-- constrained nothing here; what constrains them is furniture, rock
|
||||
-- pillars and how small the rooms are.
|
||||
["AGATHAS_ROOM"] = { x = 2, y = 1, shape = "narrow", cam = "wide" },
|
||||
["BRUNOS_ROOM"] = { x = 3, y = 1, shape = "narrow" },
|
||||
["CELADON_GYM"] = { x = 0, y = 3, shape = "narrow" },
|
||||
["CERULEAN_CAVE_1F"] = { x = 1, y = 7, shape = "narrow" },
|
||||
["AGATHAS_ROOM"] = { x = 4, y = 2, shape = "narrow", cam = "wide" },
|
||||
["BRUNOS_ROOM"] = { x = 2, y = 1, shape = "wide", turn = 90 },
|
||||
["CELADON_GYM"] = { x = 3, y = 4,
|
||||
shape = "narrow", turn = 90, cam = "wide" },
|
||||
["CERULEAN_CAVE_1F"] = { x = 12, y = 8, shape = "narrow", cam = "wide" },
|
||||
-- 2F is a maze of one-cell rock corridors; all 24 of its candidate spots
|
||||
-- hide a mon, so it borrows the floor below -- the same cave
|
||||
["CERULEAN_CAVE_2F"] = { map = "CERULEAN_CAVE_B1F", x = 2, y = 0,
|
||||
shape = "wide" },
|
||||
["CERULEAN_CAVE_B1F"] = { x = 2, y = 0, shape = "wide" },
|
||||
["CERULEAN_GYM"] = { x = 0, y = 1, shape = "narrow" },
|
||||
["CERULEAN_CAVE_B1F"] = { x = 2, y = 0, shape = "wide", cam = "wide" },
|
||||
["CERULEAN_GYM"] = { x = 4, y = 2, shape = "wide" },
|
||||
["CHAMPIONS_ROOM"] = { x = 2, y = 2, shape = "narrow", cam = "wide" },
|
||||
["CINNABAR_GYM"] = { x = 18, y = 10, shape = "narrow" },
|
||||
["DIGLETTS_CAVE"] = { x = 19, y = 16, shape = "wide" },
|
||||
["FIGHTING_DOJO"] = { x = 4, y = 1, shape = "narrow" },
|
||||
["LANCES_ROOM"] = { x = 5, y = 15, shape = "wide" },
|
||||
["LORELEIS_ROOM"] = { x = 5, y = 2, shape = "narrow" },
|
||||
["MT_MOON_1F"] = { x = 24, y = 17, shape = "wide" },
|
||||
["MT_MOON_B1F"] = { x = 5, y = 12, shape = "wide" },
|
||||
["MT_MOON_B2F"] = { x = 2, y = 16, shape = "wide" },
|
||||
["CINNABAR_GYM"] = { x = 9, y = 16,
|
||||
shape = "narrow", turn = 90, cam = "wide" },
|
||||
["DIGLETTS_CAVE"] = { x = 14, y = 26,
|
||||
shape = "wide", turn = 90, cam = "wide" },
|
||||
["FIGHTING_DOJO"] = { x = 3, y = 5,
|
||||
shape = "narrow", turn = 90, cam = "wide" },
|
||||
["LANCES_ROOM"] = { x = 4, y = 2, shape = "wide", cam = "wide" },
|
||||
["LORELEIS_ROOM"] = { x = 4, y = 2, shape = "narrow" },
|
||||
["MT_MOON_1F"] = { x = 25, y = 16, shape = "wide" },
|
||||
["MT_MOON_B1F"] = { x = 4, y = 11, shape = "wide" },
|
||||
["MT_MOON_B2F"] = { x = 8, y = 20, shape = "wide" },
|
||||
|
||||
-- The three gyms the default rig cannot stand back from. Five blocks is
|
||||
-- further than these rooms are wide, so the eye landed outside the map and
|
||||
@@ -190,7 +214,7 @@ return {
|
||||
-- BattleCam), which fits inside the room; the mons come out smaller and all
|
||||
-- three became stageable. It is asked for HERE, per map, so every area that
|
||||
-- does not ask keeps the long lens it was framed for.
|
||||
["FUCHSIA_GYM"] = { x = 7, y = 6, shape = "narrow", cam = "wide" },
|
||||
["PEWTER_GYM"] = { x = 4, y = 8, shape = "narrow", cam = "wide" },
|
||||
["FUCHSIA_GYM"] = { x = 8, y = 6, shape = "narrow", cam = "wide" },
|
||||
["PEWTER_GYM"] = { x = 4, y = 1, shape = "narrow", turn = 90, cam = "wide" },
|
||||
["VIRIDIAN_GYM"] = { x = 10, y = 8, shape = "narrow", cam = "wide" },
|
||||
}
|
||||
|
||||
+72
-8
@@ -109,6 +109,57 @@ BattleArena.SHAPES = {
|
||||
{ id = "narrow", w = 1, h = 4, enemy = { 0, 0 }, player = { 0, 3 } },
|
||||
}
|
||||
|
||||
-- ------- which way round the fight stands
|
||||
--
|
||||
-- Both shapes above are drawn north-south, with the foe at the top and the
|
||||
-- player below it, and the camera is solved for that: it sits off the
|
||||
-- player's shoulder, low and back down the arena's own axis. `turn` swings
|
||||
-- the WHOLE staging a quarter at a time -- the footprint, the two cells, and
|
||||
-- the camera with them -- so the composition on screen is identical and only
|
||||
-- the ground under it is different.
|
||||
--
|
||||
-- It buys two things.
|
||||
--
|
||||
-- A footprint that FITS. The wide shape is three cells by six; an east-west
|
||||
-- corridor two cells deep has no room for it standing up and all the room in
|
||||
-- the world for it lying down. Half the maps in Kanto run the other way from
|
||||
-- the one shape this mode was drawn in.
|
||||
--
|
||||
-- And a BACKDROP. A quarter turn moves the camera to a different side of the
|
||||
-- same patch of ground, so the wall behind the pair becomes the window
|
||||
-- behind them, or the cliff becomes the valley. Nothing about the shot's
|
||||
-- geometry changes -- the mons land on the same two screen anchors at the
|
||||
-- same size -- so this is purely a choice about what is behind them, made
|
||||
-- per map by somebody looking at it.
|
||||
--
|
||||
-- Written in DEGREES in data/battle_arenas.lua (`turn = 90`) because that is
|
||||
-- what it is; handled as quarter turns everywhere below.
|
||||
local function quarters(turn)
|
||||
local q = math.floor(((tonumber(turn) or 0) / 90) + 0.5)
|
||||
return ((q % 4) + 4) % 4
|
||||
end
|
||||
|
||||
BattleArena.quarters = quarters
|
||||
|
||||
-- The footprint a shape covers once turned: a quarter or three of a turn
|
||||
-- swaps how far it reaches in each direction, which is the whole reason a
|
||||
-- corridor takes one and not the other.
|
||||
function BattleArena.extent(shape, turn)
|
||||
if quarters(turn) % 2 == 1 then return shape.h, shape.w end
|
||||
return shape.w, shape.h
|
||||
end
|
||||
|
||||
-- Where a cell offset inside the shape ends up under the same turn, measured
|
||||
-- from the turned footprint's own north-west corner -- so the corner an entry
|
||||
-- names stays the corner, whichever way the fight faces from it.
|
||||
local function spin(shape, turn, ox, oy)
|
||||
local q = quarters(turn)
|
||||
if q == 1 then return shape.h - 1 - oy, ox end
|
||||
if q == 2 then return shape.w - 1 - ox, shape.h - 1 - oy end
|
||||
if q == 3 then return oy, shape.w - 1 - ox end
|
||||
return ox, oy
|
||||
end
|
||||
|
||||
-- Whether a cell is open ground for the purpose above.
|
||||
--
|
||||
-- "Open" is the walk test the player themselves answer to, so an arena can
|
||||
@@ -174,12 +225,19 @@ end
|
||||
|
||||
-- Build the record the renderer reads: the two mons' cells and, in world
|
||||
-- pixels, the centre of each and of the pair.
|
||||
local function place(shape, x, y)
|
||||
local ex, ey = x + shape.enemy[1], y + shape.enemy[2]
|
||||
local px, py = x + shape.player[1], y + shape.player[2]
|
||||
local function place(shape, x, y, turn)
|
||||
local eox, eoy = spin(shape, turn, shape.enemy[1], shape.enemy[2])
|
||||
local pox, poy = spin(shape, turn, shape.player[1], shape.player[2])
|
||||
local ex, ey = x + eox, y + eoy
|
||||
local px, py = x + pox, y + poy
|
||||
local w, h = BattleArena.extent(shape, turn)
|
||||
local arena = {
|
||||
shape = shape.id,
|
||||
x = x, y = y, w = shape.w, h = shape.h,
|
||||
-- carried in degrees, so everything downstream that reasons about the
|
||||
-- shot -- the camera's base yaw above all -- reads the same number the
|
||||
-- data file was written with
|
||||
turn = quarters(turn) * 90,
|
||||
x = x, y = y, w = w, h = h,
|
||||
enemyCell = { ex, ey },
|
||||
playerCell = { px, py },
|
||||
-- world-pixel centres of the two cells a mon stands on
|
||||
@@ -302,8 +360,12 @@ function BattleArena.find(map, fromX, fromY, surfing)
|
||||
-- ocean rather than on a scrap of beach at the edge of the map. Land
|
||||
-- entries are unaffected: land passes the test either way.
|
||||
local grid, gw = openGrid(host, true)
|
||||
if fits(grid, gw, pick.x, pick.y, shape.w, shape.h) then
|
||||
local arena = place(shape, pick.x, pick.y)
|
||||
-- measured against the TURNED footprint: an entry that lies the arena
|
||||
-- down an east-west corridor covers different ground from the one that
|
||||
-- stands it up, and the fit test is the thing that has to know
|
||||
local fw, fh = BattleArena.extent(shape, pick.turn)
|
||||
if fits(grid, gw, pick.x, pick.y, fw, fh) then
|
||||
local arena = place(shape, pick.x, pick.y, pick.turn)
|
||||
arena.map = host
|
||||
-- which camera rig this spot is framed for; nil is the default long
|
||||
-- lens, "close" the short one small rooms need (see BattleCam)
|
||||
@@ -321,9 +383,11 @@ end
|
||||
-- The arena at a given north-west corner, whatever the map says about it.
|
||||
-- The authoring tool's manual override: a spot chosen by eye rather than by
|
||||
-- the search, so it can be photographed and judged before it is written down.
|
||||
function BattleArena.at(x, y, shapeId)
|
||||
function BattleArena.at(x, y, shapeId, turn)
|
||||
for _, shape in ipairs(BattleArena.SHAPES) do
|
||||
if shape.id == (shapeId or "wide") then return place(shape, x, y) end
|
||||
if shape.id == (shapeId or "wide") then
|
||||
return place(shape, x, y, turn)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
+21
-2
@@ -431,7 +431,22 @@ function BattleCam.rig(arena, groundY, canonical)
|
||||
-- arena's own axis, and the room the player has is all on the far side of
|
||||
-- that -- out toward square-on. (orbitRange measures exactly that room.)
|
||||
local steer = steered and -BattleCam.orbit * BattleCam.orbitRange(arena) or 0
|
||||
local yaw = steer + (fixed and 0
|
||||
-- ------- and the quarter turn the arena itself is standing at
|
||||
--
|
||||
-- An arena may be laid down any of the four ways (BattleArena's `turn`),
|
||||
-- and the rig is solved for ONE of them: eye off the player's shoulder,
|
||||
-- back down an axis that runs north-south. So the whole offset is turned
|
||||
-- with the ground under it, which leaves the camera in exactly the same
|
||||
-- place RELATIVE to the two mons -- same distance, same height, same
|
||||
-- angle -- and therefore lands them on the same two screen anchors at the
|
||||
-- same size. A turn is a fact about the map, never about the shot.
|
||||
--
|
||||
-- It goes in with the drift and the steer rather than beside them because
|
||||
-- it is the same rotation about the same point; the player's own orbit is
|
||||
-- then measured from wherever the arena starts, so both stops travel with
|
||||
-- it and side-on stays side-on.
|
||||
local base = math.rad(arena.turn or 0)
|
||||
local yaw = base + steer + (fixed and 0
|
||||
or BattleCam.PAN_YAW * phase(BattleCam.t, BattleCam.PAN_PERIOD))
|
||||
local c, s = math.cos(yaw), math.sin(yaw)
|
||||
-- the breath scales the whole offset, height included, so the eye moves
|
||||
@@ -443,7 +458,11 @@ function BattleCam.rig(arena, groundY, canonical)
|
||||
local dz = (R.side * s + R.back * c) * k
|
||||
|
||||
local eye = { mx + dx, groundY + R.height * k, mz + dz }
|
||||
local focus = { mx + R.lookX, groundY + R.lookY, mz }
|
||||
-- the aim's own offset turns with the arena too, and with the BASE alone --
|
||||
-- the drift and the steer swing the eye about the focus, so a focus that
|
||||
-- followed them would take the thing being orbited around with it
|
||||
local bc, bs = math.cos(base), math.sin(base)
|
||||
local focus = { mx + R.lookX * bc, groundY + R.lookY, mz + R.lookX * bs }
|
||||
|
||||
-- and the climb: the eye swung UP about the focus, at a constant radius.
|
||||
-- About the focus so the aim stays nailed to the two mons and only the
|
||||
|
||||
@@ -303,7 +303,11 @@ end
|
||||
-- first drawn in.
|
||||
local function shadowSignature(state, arena, terrain, nbMesh, token)
|
||||
local host = arena.map or state.map
|
||||
-- `turn` is in the signature with the corner and the shape: the same corner
|
||||
-- turned a quarter is a different footprint standing on different ground,
|
||||
-- and a cast kept from the other one freezes the shadows across it
|
||||
local parts = { "battle", host.id, arena.x, arena.y, arena.shape,
|
||||
tostring(arena.turn or 0),
|
||||
tostring(terrain), tostring(token or 0),
|
||||
-- the cycle keeps running through a fight, and an arena lit
|
||||
-- from somewhere new must be re-cast from there
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
-- data/battle_arenas.lua, edited in place by line.
|
||||
--
|
||||
-- The arena editor (tests/arena_editor.lua) writes this file back every time
|
||||
-- a map is committed, and most of what is in it is PROSE: which alternatives
|
||||
-- were rejected for each area and what was wrong with them. That argument is
|
||||
-- the part a tool cannot reconstruct, so nothing here regenerates the file --
|
||||
-- an entry that changed has its own lines replaced, a new one is appended,
|
||||
-- and every other byte comes through exactly as it was.
|
||||
--
|
||||
-- Kept out of the driver so it can be tested without a game: a bug in here
|
||||
-- writes a config file, and the failure mode of a bad one is silent -- every
|
||||
-- map loses its authored spot and battles quietly start happening somewhere
|
||||
-- else. `apply` therefore never decides on its own that its output is good;
|
||||
-- it hands back the text and the caller parses it back and checks it says
|
||||
-- what was asked for before anything is overwritten.
|
||||
--
|
||||
-- local Config = dofile("mods/DramaticShapeVoxelMod/tests/arena_config.lua")
|
||||
-- local text, report = Config.apply(oldText, { ROUTE_1 = { x = 4, y = 14,
|
||||
-- shape = "narrow" } })
|
||||
--
|
||||
-- An edit's value is an entry table, `false` for an authored refusal, or
|
||||
-- Config.REMOVE to take the entry out altogether.
|
||||
local Config = {}
|
||||
|
||||
Config.REMOVE = setmetatable({}, { __tostring = function() return "REMOVE" end })
|
||||
|
||||
-- The heading new maps are collected under, so a file worked over several
|
||||
-- sessions grows one list rather than one heading per session.
|
||||
Config.SECTION = " -- ------- added by tests/arena_editor"
|
||||
|
||||
-- Lossless both ways: a trailing newline becomes a trailing empty line, and
|
||||
-- concat puts it back.
|
||||
function Config.splitLines(text)
|
||||
local out, pos = {}, 1
|
||||
while true do
|
||||
local a, b = text:find("\n", pos, true)
|
||||
if not a then
|
||||
out[#out + 1] = text:sub(pos)
|
||||
break
|
||||
end
|
||||
out[#out + 1] = text:sub(pos, a - 1)
|
||||
pos = b + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- One entry, laid out the way the file already lays them out: on one line
|
||||
-- where it fits inside the file's own margin, and wrapped under the brace
|
||||
-- where it does not (which is how the cross-floor entries are written).
|
||||
function Config.entryLines(id, entry)
|
||||
if entry == false then
|
||||
return { (" [%q] = false,"):format(id) }
|
||||
end
|
||||
local head = (" [%q] = { "):format(id)
|
||||
local parts = {}
|
||||
if entry.map then parts[#parts + 1] = ("map = %q"):format(entry.map) end
|
||||
parts[#parts + 1] = ("x = %d"):format(entry.x)
|
||||
parts[#parts + 1] = ("y = %d"):format(entry.y)
|
||||
parts[#parts + 1] = ("shape = %q"):format(entry.shape or "wide")
|
||||
if (entry.turn or 0) ~= 0 then
|
||||
parts[#parts + 1] = ("turn = %d"):format(entry.turn)
|
||||
end
|
||||
if entry.cam then parts[#parts + 1] = ("cam = %q"):format(entry.cam) end
|
||||
local one = head .. table.concat(parts, ", ") .. " },"
|
||||
if #one <= 79 then return { one } end
|
||||
local first = entry.map and 3 or 2
|
||||
return {
|
||||
head .. table.concat(parts, ", ", 1, first) .. ",",
|
||||
(" "):rep(#head) .. table.concat(parts, ", ", first + 1) .. " },",
|
||||
}
|
||||
end
|
||||
|
||||
-- Where an id's entry starts and ends, plus how many comment lines sit
|
||||
-- directly above it. The end is the first line that closes the table, so a
|
||||
-- wrapped entry is found whole rather than half-replaced.
|
||||
function Config.findEntry(lines, id)
|
||||
local pat = '^%s*%["' .. id:gsub("(%W)", "%%%1") .. '"%]%s*='
|
||||
for i = 1, #lines do
|
||||
if lines[i]:find(pat) then
|
||||
local note = 0
|
||||
while i - note - 1 >= 1 and lines[i - note - 1]:find("^%s*%-%-") do
|
||||
note = note + 1
|
||||
end
|
||||
if lines[i]:find("=%s*false%s*,?%s*$") then return i, i, note end
|
||||
for j = i, #lines do
|
||||
if lines[j]:find("}%s*,?%s*$") then return i, j, note end
|
||||
end
|
||||
return i, i, note
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- The `}` that closes the returned table: new entries go in above it.
|
||||
function Config.closingLine(lines)
|
||||
for i = #lines, 1, -1 do
|
||||
if lines[i]:find("^}") then return i end
|
||||
end
|
||||
return #lines + 1
|
||||
end
|
||||
|
||||
-- Apply `edits` (map id -> entry | false | REMOVE) to `text`.
|
||||
--
|
||||
-- Returns the new text and a report: `changed`, `added`, `removed` and
|
||||
-- `stale` as lists of map ids. `stale` is the one worth acting on -- those
|
||||
-- entries had comment lines directly above them, and a comment above an
|
||||
-- entry that has just moved describes where it USED to be.
|
||||
function Config.apply(text, edits)
|
||||
local lines = Config.splitLines(text)
|
||||
local report = { changed = {}, added = {}, removed = {}, stale = {} }
|
||||
|
||||
local ordered = {}
|
||||
for id in pairs(edits) do ordered[#ordered + 1] = id end
|
||||
table.sort(ordered)
|
||||
|
||||
local appended = {}
|
||||
for _, id in ipairs(ordered) do
|
||||
local entry = edits[id]
|
||||
-- found again per edit rather than indexed once up front: each
|
||||
-- replacement changes the line count under every index after it
|
||||
local from, to, note = Config.findEntry(lines, id)
|
||||
if entry == Config.REMOVE then
|
||||
if from then
|
||||
for _ = from, to do table.remove(lines, from) end
|
||||
report.removed[#report.removed + 1] = id
|
||||
if note > 0 then report.stale[#report.stale + 1] = id end
|
||||
end
|
||||
elseif from then
|
||||
local new = Config.entryLines(id, entry)
|
||||
for _ = from, to do table.remove(lines, from) end
|
||||
for k = #new, 1, -1 do table.insert(lines, from, new[k]) end
|
||||
report.changed[#report.changed + 1] = id
|
||||
if note > 0 then report.stale[#report.stale + 1] = id end
|
||||
else
|
||||
appended[#appended + 1] = id
|
||||
report.added[#report.added + 1] = id
|
||||
end
|
||||
end
|
||||
|
||||
if #appended > 0 then
|
||||
local at, block = nil, {}
|
||||
for i = 1, #lines do
|
||||
if lines[i]:find("^%s*%-%-%s+%-+ added by tests/arena_editor") then
|
||||
at = i + 1
|
||||
end
|
||||
end
|
||||
if not at then
|
||||
at = Config.closingLine(lines)
|
||||
block[#block + 1] = ""
|
||||
block[#block + 1] = Config.SECTION
|
||||
end
|
||||
for _, id in ipairs(appended) do
|
||||
for _, l in ipairs(Config.entryLines(id, edits[id])) do
|
||||
block[#block + 1] = l
|
||||
end
|
||||
end
|
||||
for k = #block, 1, -1 do table.insert(lines, at, block[k]) end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n"), report
|
||||
end
|
||||
|
||||
-- Whether two entries say the same thing. `false` and nil are values here --
|
||||
-- a refusal is not the absence of one -- so this is an equality test and not
|
||||
-- a truthiness one.
|
||||
function Config.same(a, b)
|
||||
if a == b then return true end
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return false end
|
||||
return a.x == b.x and a.y == b.y
|
||||
and (a.shape or "wide") == (b.shape or "wide")
|
||||
and (a.turn or 0) == (b.turn or 0)
|
||||
and a.cam == b.cam and a.map == b.map
|
||||
end
|
||||
|
||||
-- Parse a config back and check it says what was committed. This is what
|
||||
-- stands between a bug in the surgery above and a data file that no longer
|
||||
-- loads, so it is run on the TEXT before that text replaces anything.
|
||||
-- Returns the parsed table, or nil and why not.
|
||||
function Config.verify(text, edits, chunkName)
|
||||
local chunk, err = load(text, "@" .. (chunkName or "battle_arenas"))
|
||||
if not chunk then return nil, "would not compile: " .. tostring(err) end
|
||||
local ok, list = pcall(chunk)
|
||||
if not (ok and type(list) == "table") then
|
||||
return nil, "did not return a table: " .. tostring(list)
|
||||
end
|
||||
for id, want in pairs(edits) do
|
||||
local got = list[id]
|
||||
if want == Config.REMOVE then
|
||||
if got ~= nil then return nil, id .. " is still in the file" end
|
||||
elseif not Config.same(want, got) then
|
||||
return nil, id .. " did not read back as it was committed"
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
return Config
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2909,6 +2909,109 @@ T.check(math.abs(cex - 124) < 1 and math.abs(cey - 56) < 1,
|
||||
T.check(span(closeRig, shot.player) < span(rig, shot.player),
|
||||
"the mons render smaller on it, which is what it trades for fitting")
|
||||
|
||||
-- ------- the quarter turns
|
||||
--
|
||||
-- An arena may be laid down any of the four ways (BattleArena's `turn`), and
|
||||
-- the whole claim of the feature is that this is a fact about the GROUND and
|
||||
-- never about the shot: the footprint, the two mons and the camera turn
|
||||
-- together, so the pair land on the same two anchors at the same size and
|
||||
-- only what is behind them changes.
|
||||
--
|
||||
-- Asserted by REPROJECTING each turn through the real rig, exactly the way
|
||||
-- the unturned shot is checked above -- so a rig retune, or a sign error in
|
||||
-- the rotation, says so here rather than in a screenshot nobody takes.
|
||||
--
|
||||
-- In its own scope: the suite's main chunk sits at LuaJIT's 200-active-local
|
||||
-- ceiling, and every local below would be one more of them. The leading
|
||||
-- semicolon is the file's own convention -- without it the previous
|
||||
-- statement's `)` and this `(` parse as one call.
|
||||
;(function()
|
||||
local corner = { shot.x, shot.y }
|
||||
-- How wide a mon's own square comes out, measured ACROSS the arena's axis.
|
||||
-- `span` above offsets along world X, which is across the axis only while
|
||||
-- the arena is standing the way the mode was drawn; on the odd quarters the
|
||||
-- axis IS X, so offsetting along it would measure the square's depth --
|
||||
-- foreshortened by a camera that is looking almost straight down it -- and
|
||||
-- report a shrinking mon that is nothing of the kind.
|
||||
local function acrossSpan(cam, point, turn)
|
||||
local d = (turn % 180 == 0) and { 8, 0 } or { 0, 8 }
|
||||
local a = project(cam, { point[1] - d[1], point[2] - d[2] })
|
||||
local b = project(cam, { point[1] + d[1], point[2] + d[2] })
|
||||
return math.abs(b - a)
|
||||
end
|
||||
for _, turn in ipairs({ 0, 90, 180, 270 }) do
|
||||
BattleCam.reset()
|
||||
local a = BattleArena.at(corner[1], corner[2], "wide", turn)
|
||||
T.check(a ~= nil, ("the wide arena places at turn %d"):format(turn))
|
||||
T.eq(a.turn, turn, ("and carries the turn it was placed at (%d)"):format(turn))
|
||||
|
||||
-- the footprint swaps its reach on the odd quarters, which is the whole
|
||||
-- reason a corridor takes one way round and refuses the other
|
||||
local wantW = (turn % 180 == 0) and 3 or 6
|
||||
local wantH = (turn % 180 == 0) and 6 or 3
|
||||
T.eq(a.w, wantW, ("turn %d is %d cells across"):format(turn, wantW))
|
||||
T.eq(a.h, wantH, ("and %d deep"):format(wantH))
|
||||
|
||||
-- both mons stay inside the footprint they were turned within, and stay
|
||||
-- three cells apart -- the gap the move animations are scaled against
|
||||
T.check(a.enemyCell[1] >= a.x and a.enemyCell[1] < a.x + a.w
|
||||
and a.enemyCell[2] >= a.y and a.enemyCell[2] < a.y + a.h,
|
||||
("turn %d keeps the foe inside the footprint"):format(turn))
|
||||
T.check(a.playerCell[1] >= a.x and a.playerCell[1] < a.x + a.w
|
||||
and a.playerCell[2] >= a.y and a.playerCell[2] < a.y + a.h,
|
||||
("turn %d keeps the player inside it too"):format(turn))
|
||||
local gap = math.abs(a.enemyCell[1] - a.playerCell[1])
|
||||
+ math.abs(a.enemyCell[2] - a.playerCell[2])
|
||||
T.eq(gap, 3, ("turn %d still stands them three cells apart"):format(turn))
|
||||
|
||||
-- and which way the foe lies from the player, which is what `turn` NAMES
|
||||
local dx = a.enemyCell[1] - a.playerCell[1]
|
||||
local dy = a.enemyCell[2] - a.playerCell[2]
|
||||
local want = ({ [0] = { 0, -3 }, [90] = { 3, 0 },
|
||||
[180] = { 0, 3 }, [270] = { -3, 0 } })[turn]
|
||||
T.check(dx == want[1] and dy == want[2],
|
||||
("turn %d puts the foe %d,%d from the player: got %d,%d")
|
||||
:format(turn, want[1], want[2], dx, dy))
|
||||
|
||||
-- THE INVARIANT: the same composition, whichever way round it stands
|
||||
local r = BattleCam.rig(a, 0)
|
||||
local ppx, ppy = project(r, a.player)
|
||||
local eex, eey = project(r, a.enemy)
|
||||
T.check(ppx ~= nil and eex ~= nil,
|
||||
("turn %d keeps both marks in front of the camera"):format(turn))
|
||||
T.check(math.abs(ppx - 26) < 1 and math.abs(ppy - 96) < 1,
|
||||
("turn %d lands the player's mark on its anchor: (%.2f, %.2f)")
|
||||
:format(turn, ppx, ppy))
|
||||
T.check(math.abs(eex - 124) < 1 and math.abs(eey - 56) < 1,
|
||||
("turn %d lands the enemy's on its own: (%.2f, %.2f)")
|
||||
:format(turn, eex, eey))
|
||||
T.check(math.abs(acrossSpan(r, a.player, turn) - 64) < 4,
|
||||
("turn %d still makes the player's square a back pic wide (64px): got "
|
||||
.. "%.2f"):format(turn, acrossSpan(r, a.player, turn)))
|
||||
T.check(math.abs(acrossSpan(r, a.enemy, turn) - 56) < 4,
|
||||
("turn %d still makes the foe's square a front pic wide (56px): got "
|
||||
.. "%.2f"):format(turn, acrossSpan(r, a.enemy, turn)))
|
||||
|
||||
-- the eye really did move: a turn that left the camera where it was would
|
||||
-- pass every anchor test above by looking at the pair from the side
|
||||
if turn ~= 0 then
|
||||
local base = BattleArena.at(corner[1], corner[2], "wide", 0)
|
||||
BattleCam.reset()
|
||||
local r0 = BattleCam.rig(base, 0)
|
||||
local moved = math.abs(r.eye[1] - r0.eye[1])
|
||||
+ math.abs(r.eye[3] - r0.eye[3])
|
||||
T.check(moved > 16,
|
||||
("turn %d actually moves the camera (%.1f world px)"):format(turn, moved))
|
||||
end
|
||||
end
|
||||
|
||||
-- degrees in, degrees out, and anything else folded onto the four
|
||||
T.eq(BattleArena.quarters(360), 0, "a full turn is no turn")
|
||||
T.eq(BattleArena.quarters(-90), 3, "and a negative one comes round the back")
|
||||
T.eq(BattleArena.at(1, 1, "wide").turn, 0,
|
||||
"an arena placed without a turn is the way the mode was drawn")
|
||||
end)()
|
||||
|
||||
-- an arena picks its rig by name, and anything unnamed gets the default
|
||||
T.eq(BattleCam.rigFor({ cam = "wide" }), BattleCam.RIGS.wide,
|
||||
"an arena that asks for the wide lens gets it")
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
-- Driver: one STADIUM screenshot per species, every one of the 151 standing
|
||||
-- on the ENEMY mark in its standby loop.
|
||||
--
|
||||
-- stadium_shots.lua photographs a handful of deliberately awkward cases; this
|
||||
-- is the exhaustive sweep behind it. What it exists to catch is the class of
|
||||
-- fault that is per-MODEL and invisible to the headless QA pass -- a texture
|
||||
-- that comes out magenta, a mon standing in the floor or floating over it, a
|
||||
-- silhouette that is subtly the wrong shape -- none of which is a number
|
||||
-- stadium_anim_qa.lua can measure.
|
||||
--
|
||||
-- SHOT_DIR=mods/DramaticShapeVoxelMod/.claude/stadium_verify \
|
||||
-- POKEPORT_SPEED=4 \
|
||||
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/stadium_all_shots.lua \
|
||||
-- lovec.exe .
|
||||
--
|
||||
-- Files land as NNN_species.png in dex order. Run from the PROJECT ROOT.
|
||||
--
|
||||
-- ------- everything but the foe is nailed down
|
||||
--
|
||||
-- The whole value of 151 shots is that they are comparable, so the staging is
|
||||
-- identical in every one: same map, same cells, same hour (the day/night
|
||||
-- cycle would otherwise relight every shot), same rung, and the same small
|
||||
-- Pokemon on the player's mark -- DIGLETT, chosen because it is the shortest
|
||||
-- model in the set and so hides the least of the arena behind it. Whatever
|
||||
-- differs between two of these shots is the foe.
|
||||
--
|
||||
-- ------- the model/sprite verdict is the other half
|
||||
--
|
||||
-- A species whose pack will not load, or whose animation data the packer
|
||||
-- declined, does not error: StadiumMon.setSpecies answers false and the mode
|
||||
-- quietly stands the Game Boy's flat pic on the tile instead. That is the
|
||||
-- correct behaviour and it is nearly invisible in a screenshot at a glance --
|
||||
-- which is exactly why it needs counting rather than looking. Stadium.showing
|
||||
-- is asked for every species and the ones that decline are listed at the end.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or ".claude/stadium_verify"
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
|
||||
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 Battles = lib.require("OverworldBattle")
|
||||
local Stadium = lib.require("Stadium")
|
||||
local Pack = lib.require("StadiumPack")
|
||||
local Install = lib.require("StadiumInstall")
|
||||
|
||||
-- every real species the merged data carries, in dex order
|
||||
local species = {}
|
||||
for id, def in pairs(game.data.pokemon) do
|
||||
if type(id) == "string" and type(def) == "table"
|
||||
and def.dex and def.dex >= 1 and def.dex <= 151 then
|
||||
species[#species + 1] = { id = id, dex = def.dex }
|
||||
end
|
||||
end
|
||||
table.sort(species, function(a, b) return a.dex < b.dex end)
|
||||
|
||||
game.save.player.name = "RED"
|
||||
|
||||
-- ONTO THE MAP FIRST: the model build (if one is owed) is pushed from the
|
||||
-- mod's update hook on the first frame the overworld is the top state, so
|
||||
-- waiting for it before teleporting waits forever.
|
||||
U.teleport(game, "ROUTE_1", 5, 8, "down")
|
||||
|
||||
local waited = 0
|
||||
while (Install.pending() or Install.status.state == "building")
|
||||
and waited < 5400 do
|
||||
U.wait(10)
|
||||
waited = waited + 10
|
||||
end
|
||||
if waited > 0 then
|
||||
U.log(("waited %.1fs for the model build -- %s")
|
||||
:format(waited / 60, tostring(Install.status.state)))
|
||||
end
|
||||
|
||||
Battles.setting:setValue("stadium", game)
|
||||
Battles.backSetting:setValue(false, game)
|
||||
lib.require("DayNight").setting:setValue(os.getenv("DS_TOD") or "day", game)
|
||||
|
||||
local have = 0
|
||||
for dex = 1, 151 do
|
||||
if Pack.available(dex) then have = have + 1 end
|
||||
end
|
||||
U.log(("%d species, %d/151 packs on disk, rung = %s")
|
||||
:format(#species, have, tostring(Battles.setting:get())))
|
||||
|
||||
-- let the neighbourhood's meshes land, so the first fight opens on the
|
||||
-- real arena rather than the flat fallback
|
||||
U.wait(90)
|
||||
|
||||
local asPic, stuck, missed = {}, {}, {}
|
||||
|
||||
for _, s in ipairs(species) do
|
||||
-- the player's side is a constant (see the header), so the only thing
|
||||
-- that changes between two shots is the Pokemon being verified
|
||||
game.save.party = { Pokemon.new(game.data, "DIGLETT", 40) }
|
||||
|
||||
local battle = BattleState.newWild(game, s.id, 30)
|
||||
battle.onFinish = function() end
|
||||
game.overworld:pushBattle(battle)
|
||||
|
||||
-- the wipe and the intro chatter, then far enough past it that the
|
||||
-- player's own Pokemon has been sent out -- before that the player's
|
||||
-- side is legitimately the trainer's back sprite and the foe is still
|
||||
-- sliding in
|
||||
U.wait(70)
|
||||
for _ = 1, 40 do
|
||||
if battle.phase == "menu" then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
end
|
||||
if battle.phase ~= "menu" then
|
||||
stuck[#stuck + 1] = s.id
|
||||
U.log(("STUCK before menu: %s (phase %s)")
|
||||
:format(s.id, tostring(battle.phase)))
|
||||
end
|
||||
-- and let the send-out beat finish so the mon is standing still in its
|
||||
-- standby loop rather than mid-arrival
|
||||
U.wait(45)
|
||||
|
||||
-- model or flat pic? asked here, with the fight actually on screen
|
||||
if not Stadium.showing("enemy") then
|
||||
asPic[#asPic + 1] = ("%03d %s"):format(s.dex, s.id)
|
||||
end
|
||||
|
||||
local path = ("%s/%03d_%s.png"):format(DIR, s.dex, s.id:lower())
|
||||
if not U.shot(game, path) then
|
||||
missed[#missed + 1] = s.id
|
||||
end
|
||||
|
||||
while game.stack:top() and game.stack:top() ~= game.overworld do
|
||||
game.stack:pop()
|
||||
end
|
||||
U.wait(10)
|
||||
end
|
||||
|
||||
U.log(("---- %d shots -> %s"):format(#species - #missed, DIR))
|
||||
if #asPic > 0 then
|
||||
U.log(("%d species declined to a model and were shot as FLAT PICS: %s")
|
||||
:format(#asPic, table.concat(asPic, ", ")))
|
||||
else
|
||||
U.log("all species stood as MODELS -- none fell back to a flat pic")
|
||||
end
|
||||
if #stuck > 0 then
|
||||
U.log(("%d never reached the battle menu: %s")
|
||||
:format(#stuck, table.concat(stuck, ", ")))
|
||||
end
|
||||
if #missed > 0 then
|
||||
U.log(("%d screenshots did not reach disk: %s")
|
||||
:format(#missed, table.concat(missed, ", ")))
|
||||
end
|
||||
U.log("done -- " .. DIR)
|
||||
end
|
||||
Reference in New Issue
Block a user