add file picker for stadium
This commit is contained in:
+46
-3
@@ -96,9 +96,30 @@
|
||||
mod ships no Pokemon Stadium data and cannot: it is that game's. What it
|
||||
ships is the READER.
|
||||
|
||||
Drop a Pokemon Stadium (US) ROM -- `.z64`, `.n64` or `.v64`, the byte
|
||||
order is detected -- into a `baseroms/` folder beside the game, straight in
|
||||
it rather than in a revision subfolder under it, and the first time it runs
|
||||
**Press STADIUM ROM on the OPTIONS menu and pick the file.** The row opens
|
||||
the host's own file dialog -- osascript on macOS, PowerShell's
|
||||
OpenFileDialog on Windows, zenity then kdialog on Linux, which are the same
|
||||
four the engine's own Game Boy importer uses -- and the models are built
|
||||
from whatever comes back. `.z64`, `.n64` and `.v64` all work; the byte
|
||||
order is detected. The row reads IMPORT before and READY after, and
|
||||
pressing it again imports a different cartridge.
|
||||
|
||||
The ROM is **not kept**: it is read, built from, and forgotten. A Stadium
|
||||
cartridge is 32 MB and the models built out of it are 34, so keeping both
|
||||
would double the cost of the feature for a file with no further use -- the
|
||||
marker still records its md5, so a swapped cartridge is noticed.
|
||||
|
||||
The wrong file is refused with a reason on the loading screen rather than
|
||||
half-built, and refused BEFORE anything is written -- which matters more
|
||||
than it sounds, because the marker is the only thing that makes 151 files
|
||||
on disk count as installed, so a refusal that wrote one anyway would
|
||||
uninstall a working set.
|
||||
|
||||
The original route still works everywhere, and is the answer on the
|
||||
platforms with no dialog (Android; a Linux install with neither zenity nor
|
||||
kdialog): drop a Pokemon Stadium (US) ROM into a `baseroms/` folder beside
|
||||
the game, straight in it rather than in a revision subfolder under it, and
|
||||
the first time it runs
|
||||
the 151 battle models are built out of it on a loading screen, in about ten
|
||||
seconds, one species a frame. The screen says what it is doing in those
|
||||
words -- **ONE-TIME EXTRACTION OF STADIUM ASSETS** -- carries a progress bar
|
||||
@@ -234,6 +255,28 @@
|
||||
the shot driver's `DS_FAINT` case prints the frame the bar empties against
|
||||
the frame the animation starts, and they are the same frame.
|
||||
|
||||
- **FLY and DIG now take the model off the field.** The charging turn of a
|
||||
two-turn move puts the Pokemon out of reach, and the engine says so through
|
||||
`picFx[battler].hidden` -- FLY runs `SE_SLIDE_MON_OFF` and DIG
|
||||
`SE_SLIDE_MON_DOWN`, each a 19-24 frame slide that ends by setting that
|
||||
flag, and the release turn puts the pic back. The model was reading
|
||||
`fxHidden`, which is the damage BLINK and nothing else, so it stood on its
|
||||
tile while the game insisted it was underground -- and insisted in the
|
||||
strongest way it has, by making every attack aimed at it miss.
|
||||
|
||||
It now reads the same field the pic does, which also covers every other
|
||||
vanishing act on that seam: the user of Explosion, a Pokemon Teleported
|
||||
away. Read as the engine's own answer rather than as a list of move ids,
|
||||
so a mod that adds a third two-turn move gets it for free.
|
||||
|
||||
It is deliberately NOT held to the end of its own animation the way a
|
||||
collapse is. The Stadium animations are authored as the WHOLE move --
|
||||
Charizard's DIG is 3.83 seconds of burrow, emerge and strike, because
|
||||
Stadium plays it in one turn -- so cutting at the engine's own hide shows
|
||||
the burrowing and holds the strike back for the turn it actually lands on.
|
||||
Verified as a timeline (`DS_FLY`, and `DS_MOVE=DIG`): the pic hid at frame
|
||||
67, the model went with it, and both came back at 264.
|
||||
|
||||
- **And the faint animation now gets to finish.** A fainted Pokemon left the
|
||||
field when its PIC did, and the engine's pic slide is fourteen frames of a
|
||||
60 Hz clock -- `SlideDownFaintedMonPic`, seven rows two frames apart, under
|
||||
|
||||
@@ -124,14 +124,28 @@ under that cave's void and its own flat light.
|
||||
data. What ships is the reader; you supply the cartridge, exactly as this
|
||||
engine already asks you to supply the Game Boy ROM it is a recompilation of.
|
||||
|
||||
1. Put a **Pokémon Stadium (US)** ROM in a `baseroms/` folder beside the game
|
||||
— straight in it, not in a subfolder. `.z64`, `.n64` and `.v64` all work;
|
||||
the byte order is detected.
|
||||
In a packaged build, `baseroms/` goes in the save directory; the mod logs
|
||||
the exact path on startup when it cannot find one.
|
||||
2. Start the game. The 151 models are built out of the ROM on a loading
|
||||
screen that says so and shows a progress bar, in about ten seconds.
|
||||
3. The two STADIUM rungs appear on the 3D-BTL row.
|
||||
1. Open **OPTIONS** and press the **STADIUM ROM** row. It opens your system's
|
||||
file picker; choose a **Pokémon Stadium (US)** ROM. `.z64`, `.n64` and
|
||||
`.v64` all work — the byte order is detected, and the wrong file is
|
||||
refused with a reason rather than half-built.
|
||||
2. The 151 models are built on a loading screen that says so and shows a
|
||||
progress bar, in about ten seconds. The row then reads **READY**.
|
||||
|
||||
The ROM itself is **not kept** — it is read, built from, and forgotten, so
|
||||
the cartridge does not sit in your save directory alongside the models it
|
||||
produced. Press the row again any time to import a different one.
|
||||
|
||||
There is no picker on Android, or on a Linux install with neither `zenity`
|
||||
nor `kdialog`. Those keep the original route, which still works everywhere:
|
||||
|
||||
- Put the ROM in a `baseroms/` folder beside the game — straight in it, not
|
||||
in a subfolder — and start the game.
|
||||
- In a packaged build (and on Android) `baseroms/` goes in the save
|
||||
directory; the mod logs the exact path on startup when it cannot find one.
|
||||
On Android that is the app's external-files folder, reachable over USB or
|
||||
any file manager without root.
|
||||
|
||||
Either way, the two STADIUM rungs appear on the 3D-BTL row when it's done.
|
||||
|
||||
The built models live in the save directory, not in the mod folder, and are
|
||||
rebuilt automatically if the format changes or the ROM does. Until they exist
|
||||
|
||||
@@ -205,6 +205,38 @@ local function onField(battle, side, mon)
|
||||
end
|
||||
local ok, hidden = pcall(battle.fxHidden, battle, battler)
|
||||
if ok and hidden then return false end
|
||||
-- ------- FLY and DIG: the Pokemon that is not there
|
||||
--
|
||||
-- `fxHidden` above is the damage BLINK and nothing else. The other way a
|
||||
-- Pokemon leaves the screen -- the important one -- is the engine's
|
||||
-- per-battler pic program, `picFx`, and that is where the two-turn moves
|
||||
-- live: FLY runs SE_SLIDE_MON_OFF and DIG SE_SLIDE_MON_DOWN on the charge
|
||||
-- turn, each a 19-24 frame slide that ENDS by setting `hidden`, and the
|
||||
-- release turn puts the pic back through SE_SLIDE_MON_UP /
|
||||
-- SE_SHOW_MON_PIC. Every other vanishing act is the same field: the user
|
||||
-- of Explosion, a Pokemon that has been Teleported away.
|
||||
--
|
||||
-- Without this the model simply stood on its tile while the game said it
|
||||
-- was underground -- and said it in the strongest way it has, by making
|
||||
-- every attack aimed at it miss. That is the one thing in the frame
|
||||
-- contradicting the battle it is part of.
|
||||
--
|
||||
-- Read as the engine's own answer rather than as a list of moves: this
|
||||
-- mode's whole method is to let the battle decide and follow it, and a
|
||||
-- table of move ids here would be a second place for the same facts to
|
||||
-- live and would go stale against a mod that adds a third one.
|
||||
--
|
||||
-- The engine's slide is 19-24 frames, so the model plays the opening of
|
||||
-- its own FLY or DIG animation while the pic slides and is gone when the
|
||||
-- pic is. It is NOT held to the end of that animation the way a collapse
|
||||
-- is (see below), and the difference is not an oversight: the Stadium
|
||||
-- animations are authored as the WHOLE move -- Charizard's DIG is 3.83
|
||||
-- seconds of burrow, emerge and hit -- because Stadium plays it in one
|
||||
-- turn. Gen 1 splits it across two, so cutting at the engine's own hide
|
||||
-- shows the burrowing and holds the strike back for the turn it lands on,
|
||||
-- which is the right half of the animation for the turn being played.
|
||||
local pf = battle.picFx and battle.picFx[battler]
|
||||
if pf and pf.hidden then return false end
|
||||
if battler.fainted then
|
||||
local okF, sliding = pcall(battle.fxFaintActive, battle, battler)
|
||||
if okF and sliding then return true end
|
||||
|
||||
+61
-7
@@ -196,8 +196,8 @@ local function writePack(species, bytes)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Open the ROM and start a stepped build. Returns false plus a reason when
|
||||
-- there is nothing to build from.
|
||||
-- Open the ROM found in `baseroms/` and start a stepped build. Returns false
|
||||
-- plus a reason when there is nothing to build from.
|
||||
function StadiumInstall.begin()
|
||||
local f = fs()
|
||||
if not f then return false, "no filesystem" end
|
||||
@@ -208,6 +208,23 @@ function StadiumInstall.begin()
|
||||
if not (okRead and type(bytes) == "string") then
|
||||
return false, "could not read " .. path
|
||||
end
|
||||
return StadiumInstall.beginFrom(bytes, path)
|
||||
end
|
||||
|
||||
-- The same, from bytes somebody else has already got hold of -- which is the
|
||||
-- IMPORTED path (StadiumRomPick), where the file is at an absolute location
|
||||
-- love.filesystem cannot see and was read with io.open.
|
||||
--
|
||||
-- The two entry points share everything from here down on purpose: an
|
||||
-- imported cartridge and a dropped one produce the same 151 files, the same
|
||||
-- marker and the same md5, so there is exactly one build in this mod and no
|
||||
-- second one to keep in step.
|
||||
--
|
||||
-- `label` is only ever used to say WHICH file a complaint is about.
|
||||
function StadiumInstall.beginFrom(bytes, label)
|
||||
local f = fs()
|
||||
if not f then return false, "no filesystem" end
|
||||
if type(bytes) ~= "string" or #bytes == 0 then return false, "empty file" end
|
||||
|
||||
local StadiumRom = V.require("StadiumRom")
|
||||
local StadiumBuild = V.require("StadiumBuild")
|
||||
@@ -215,8 +232,30 @@ function StadiumInstall.begin()
|
||||
if not rom then return false, tostring(err) end
|
||||
if not rom:isExpectedUS() then
|
||||
V.mod.log:warn("stadium: %s is md5 %s, not the US 1.0 ROM the model "
|
||||
.. "offsets are keyed to -- building anyway", path,
|
||||
tostring(rom:md5()))
|
||||
.. "offsets are keyed to -- building anyway",
|
||||
tostring(label or "the ROM"), tostring(rom:md5()))
|
||||
end
|
||||
|
||||
-- ------- refuse a ROM with no models in it, BEFORE anything is written
|
||||
--
|
||||
-- A file picker invites the wrong file -- most obviously the Game Boy
|
||||
-- cartridge the player already imported once -- and the reader's answer to
|
||||
-- one is a model count of zero. That has to be caught HERE rather than
|
||||
-- allowed to become an empty build, because an empty build is
|
||||
-- indistinguishable from a finished one further down: `job.total` is
|
||||
-- clamped to the count, `step` completes on the first call with nothing
|
||||
-- attempted and therefore nothing FAILED, and the marker gets written
|
||||
-- saying `DSM3 0`.
|
||||
--
|
||||
-- On a fresh machine that is merely a lie on the loading screen -- READY,
|
||||
-- with no models. On one that already HAD them it is worse: the marker is
|
||||
-- the only thing that makes 151 files on disk count as installed, so
|
||||
-- overwriting it with a zero uninstalls a good set and the STADIUM rungs
|
||||
-- vanish off the row. Nothing below this line runs for a file that cannot
|
||||
-- possibly produce a build.
|
||||
local models = rom:modelCount()
|
||||
if not (models and models >= StadiumInstall.COUNT) then
|
||||
return false, "that is not a Pokemon Stadium ROM"
|
||||
end
|
||||
|
||||
pcall(f.createDirectory, StadiumInstall.DIR)
|
||||
@@ -243,7 +282,12 @@ function StadiumInstall.step()
|
||||
end
|
||||
if not more then
|
||||
local f = fs()
|
||||
local wrote = #job.failed == 0
|
||||
-- `job.total > 0` as well as "nothing failed", because a job with nothing
|
||||
-- IN it satisfies the second on its own -- and the marker this writes is
|
||||
-- what makes a set count as installed, so it must never be written for a
|
||||
-- build that did not happen. beginFrom refuses such a ROM outright; this
|
||||
-- is the same rule stated where the consequence is.
|
||||
local wrote = #job.failed == 0 and job.total > 0
|
||||
if wrote and f then
|
||||
pcall(f.write, StadiumInstall.MARKER,
|
||||
("%s %d %s\n"):format(StadiumInstall.FORMAT, job.total,
|
||||
@@ -253,8 +297,18 @@ function StadiumInstall.step()
|
||||
end
|
||||
if not wrote then
|
||||
status.state = "failed"
|
||||
status.error = ("%d of %d models could not be built")
|
||||
:format(#job.failed, job.total)
|
||||
-- EVERY species failing is not a bad build, it is the wrong file: the
|
||||
-- offsets the reader walks are Pokemon Stadium's, so a different game
|
||||
-- -- or the Game Boy cartridge the player already imported once, which
|
||||
-- is the mistake a file picker invites -- misses on all 151 rather than
|
||||
-- on a few. Worth telling apart, because "0 of 151 models were built"
|
||||
-- reads as a broken mod and this reads as a wrong click.
|
||||
if #job.failed >= job.total then
|
||||
status.error = "that is not a Pokemon Stadium ROM"
|
||||
else
|
||||
status.error = ("%d of %d models could not be built")
|
||||
:format(#job.failed, job.total)
|
||||
end
|
||||
else
|
||||
status.state = "done"
|
||||
end
|
||||
|
||||
+57
-8
@@ -68,6 +68,34 @@ local function centred(str, y)
|
||||
text(str, (W - F.width(str)) / 2, y)
|
||||
end
|
||||
|
||||
-- How many glyphs fit across the frame. The font is a fixed eight pixels, so
|
||||
-- twenty is the line -- and a centred string longer than that does not
|
||||
-- overflow tidily off one side, it clips off BOTH and loses its first word as
|
||||
-- well as its last ("that is not a Pokemon Stadium ROM" came out as "at is
|
||||
-- not a Pokemon").
|
||||
local COLS = 20
|
||||
|
||||
-- Break a reason into lines that fit, on word boundaries, and never more than
|
||||
-- `limit` of them -- the plate has room for two and a message nobody can read
|
||||
-- the end of is not improved by adding a third.
|
||||
local function wrapped(str, limit)
|
||||
local lines, line = {}, nil
|
||||
for word in tostring(str):gmatch("%S+") do
|
||||
local try = line and (line .. " " .. word) or word
|
||||
if #try <= COLS then
|
||||
line = try
|
||||
else
|
||||
if line then lines[#lines + 1] = line end
|
||||
-- a single word too long to fit is cut rather than dropped: it is
|
||||
-- usually a filename, and its first twenty characters still identify it
|
||||
line = (#word <= COLS) and word or word:sub(1, COLS)
|
||||
end
|
||||
if #lines >= (limit or 2) then break end
|
||||
end
|
||||
if line and #lines < (limit or 2) then lines[#lines + 1] = line end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ------- dex number -> the engine's own species key
|
||||
--
|
||||
-- Built once, from the loaded data rather than from a list of names carried
|
||||
@@ -91,8 +119,16 @@ local function speciesName(dex)
|
||||
return dexNames[dex]
|
||||
end
|
||||
|
||||
function StadiumScreen.new(game)
|
||||
return setmetatable({ game = game, hold = 0, started = false }, StadiumScreen)
|
||||
-- `adopt` means the caller has ALREADY started the build, or already decided
|
||||
-- it cannot start -- which is the imported path (StadiumRomPick opens the
|
||||
-- picked file itself, because love.filesystem cannot read an absolute path).
|
||||
-- Without it this screen would call begin() on the way in and throw away the
|
||||
-- job it was pushed to display, or overwrite the failure it was pushed to
|
||||
-- explain with a fresh "no ROM in baseroms" -- which would be true, and would
|
||||
-- have nothing to do with what just went wrong.
|
||||
function StadiumScreen.new(game, adopt)
|
||||
return setmetatable({ game = game, hold = 0, started = adopt and true or false,
|
||||
adopted = adopt and true or false }, StadiumScreen)
|
||||
end
|
||||
|
||||
-- Opaque: the loading screen owns the frame, so the map underneath is not
|
||||
@@ -100,6 +136,7 @@ end
|
||||
StadiumScreen.isOpaque = true
|
||||
|
||||
function StadiumScreen:enter()
|
||||
if self.adopted then return end
|
||||
local ok, err = StadiumInstall.begin()
|
||||
self.started = ok and true or false
|
||||
if not ok then
|
||||
@@ -156,10 +193,10 @@ function StadiumScreen:draw()
|
||||
centred("OF STADIUM ASSETS", 46)
|
||||
|
||||
if status.state == "failed" then
|
||||
centred("COULD NOT BUILD", 70)
|
||||
local why = tostring(status.error or "unknown"):sub(1, 24)
|
||||
centred(why, 84)
|
||||
centred("STADIUM IS OFF", 104)
|
||||
centred("COULD NOT BUILD", 68)
|
||||
local lines = wrapped(status.error or "unknown", 2)
|
||||
for i, line in ipairs(lines) do centred(line, 82 + (i - 1) * 10) end
|
||||
centred("STADIUM IS OFF", 110)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
@@ -226,9 +263,21 @@ function StadiumScreen.maybePush()
|
||||
-- they need is an absolute path that depends on how the game was
|
||||
-- installed, so it cannot be written into the options help text.
|
||||
if not StadiumInstall.available() then
|
||||
-- The IMPORT row is the answer wherever a file dialog can be opened,
|
||||
-- and it is the better one: no folder to create, no path to get right,
|
||||
-- no restart. The folder is still said, once, for the platforms with no
|
||||
-- dialog (Android, a handheld Linux with neither zenity nor kdialog)
|
||||
-- and for anyone who would rather drop a file than click through one.
|
||||
local okPick, pick = pcall(V.require, "StadiumRomPick")
|
||||
local canPick = okPick and pick and pick.available()
|
||||
V.mod.log:info("stadium: no Pokemon Stadium ROM found, so the STADIUM "
|
||||
.. "battle rungs are off. Put one (.z64/.n64/.v64) in "
|
||||
.. "%s and restart.", StadiumInstall.romHint())
|
||||
.. "battle rungs are off. %s",
|
||||
canPick
|
||||
and ("Import one from OPTIONS -> " .. pick.LABEL
|
||||
.. ", or put a .z64/.n64/.v64 in "
|
||||
.. StadiumInstall.romHint() .. " and restart.")
|
||||
or ("Put one (.z64/.n64/.v64) in "
|
||||
.. StadiumInstall.romHint() .. " and restart."))
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -795,6 +795,15 @@ mod.hooks:wrap("ui.options.rows", function(next, game, rows)
|
||||
and (not entry.when or entry.when())
|
||||
if offered then extra[#extra + 1] = entry[1]:row() end
|
||||
end
|
||||
-- and the ROM import, which is an ACTION and not a setting: there is no
|
||||
-- rung to store, nothing for the mod manager's page to persist and nothing
|
||||
-- to restore on the next boot, so it is appended here rather than living in
|
||||
-- SETTINGS. nil on a platform with no file dialog, which takes it off the
|
||||
-- menu rather than offering a button that cannot do anything.
|
||||
local okPick, importRow = pcall(function()
|
||||
return V.require("StadiumRomPick").row()
|
||||
end)
|
||||
if okPick and importRow then extra[#extra + 1] = importRow end
|
||||
return insertGrouped(out, extra)
|
||||
end)
|
||||
|
||||
|
||||
+1
-1
@@ -15,6 +15,6 @@
|
||||
"engine_internals"
|
||||
],
|
||||
"affects_link": false,
|
||||
"description": "A full 3D diorama overworld: extruded terrain, depth-buffered occlusion, voxel characters and a tilt-shift miniature pass -- and battles fought on the map itself, shot over the shoulder at the nearest clear ground with a slow parallax drift and a depth-of-field pass. Water reflects the sky, the sun, the moon and -- through a screen-space ray march -- the shoreline standing behind it. The 3D-BTL row is five rungs across two choices: 2D-3D stands the game's own battle pics up as cards and STADIUM replaces them with the Pokemon Stadium battle models -- 148 of the 151 species, animated, playing the animation the move being used calls for -- while A stages the fight on the map and B on two carried discs against the sky, which works on every map including the ones with nowhere to put a fight. The STADIUM rungs need a Pokemon Stadium (US) ROM of your own dropped in a baseroms/ folder, and build themselves out of it on first run. Registers two render pipelines and claims hotkeys 3, 5, 6, 7, 8 and 9 -- 3 and 5 displace the engine's TILT and GBC FX keys, both still reachable on the OPTIONS menu. Presentational only: it changes what a battle is drawn over, never where anybody stands.",
|
||||
"description": "A full 3D diorama overworld: extruded terrain, depth-buffered occlusion, voxel characters and a tilt-shift miniature pass -- and battles fought on the map itself, shot over the shoulder at the nearest clear ground with a slow parallax drift and a depth-of-field pass. Water reflects the sky, the sun, the moon and -- through a screen-space ray march -- the shoreline standing behind it. The 3D-BTL row is five rungs across two choices: 2D-3D stands the game's own battle pics up as cards and STADIUM replaces them with the Pokemon Stadium battle models -- 148 of the 151 species, animated, playing the animation the move being used calls for -- while A stages the fight on the map and B on two carried discs against the sky, which works on every map including the ones with nowhere to put a fight. The STADIUM rungs need a Pokemon Stadium (US) ROM of your own: import it with the system file picker from the STADIUM ROM row on the OPTIONS menu, or drop it in a baseroms/ folder, and the models build themselves out of it on a loading screen. Registers two render pipelines and claims hotkeys 3, 5, 6, 7, 8 and 9 -- 3 and 5 displace the engine's TILT and GBC FX keys, both still reachable on the OPTIONS menu. Presentational only: it changes what a battle is drawn over, never where anybody stands.",
|
||||
"github": "DramaticShape/DramaticShapeVoxelMod"
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ return {
|
||||
"WATER on FULL ray-marches the depth buffer per water pixel, so a map that is mostly sea costs real fill rate on a weak GPU -- SKY is the same look minus the ray march, and OFF is the flat water",
|
||||
"a screen-space reflection can only reflect what is in the frame: a tree just off the top edge is not in the water below it, and a ray that runs off the side fades into the sky rather than ending on a line",
|
||||
"a map with no 3x6 clearing falls back to a 1x4 one, and a map with neither draws the plain battle screen",
|
||||
"the STADIUM rungs need the Pokemon Stadium battle models, and the mod ships none of them -- they are that game's data. Drop a Pokemon Stadium (US) ROM in a baseroms/ folder beside the game and the 151 models are built out of it on a loading screen the first time it runs, in about ten seconds, into the save directory; until then the two rungs are simply not on the row. Once built, a rung declines per POKEMON rather than per battle: a species with no pack, a standing substitute doll, and the trainer's own pic before the send-out each fall back to the flat card on that side alone, with the other side keeping its model",
|
||||
"the STADIUM rungs need the Pokemon Stadium battle models, and the mod ships none of them -- they are that game's data. Press STADIUM ROM on the OPTIONS menu to pick one with the system file dialog, or drop a Pokemon Stadium (US) ROM in a baseroms/ folder beside the game; either way the 151 models are built out of it on a loading screen, in about ten seconds, into the save directory, and the ROM itself is not kept. Until then the two rungs are simply not on the row. Once built, a rung declines per POKEMON rather than per battle: a species with no pack, a standing substitute doll, and the trainer's own pic before the send-out each fall back to the flat card on that side alone, with the other side keeping its model",
|
||||
"the STADIUM rungs size a Pokemon by its own model against the set's median, with the range compressed -- the authored heights span sixteenfold, from Caterpie to Gyarados, and a shared over-the-shoulder shot cannot hold that. The order and the feel of the differences survive; the literal ratios do not",
|
||||
"three species -- Exeggutor, Tangela and Magmar -- have standby loops that are corrupt in the source extraction, and are held at their bind pose so they stand still rather than coming apart",
|
||||
"the arena is where the CAMERA goes -- nobody is moved, so a fight staged across the map is a shot of that ground, not a trip to it",
|
||||
|
||||
@@ -1136,6 +1136,20 @@ Game.keypressed(keyGame, "7")
|
||||
T.neq(Curve.setting:get(), curveBefore, "7 cycles V-CURVE")
|
||||
|
||||
local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
|
||||
|
||||
-- The two STADIUM rungs are gated on the models being installed, and they are
|
||||
-- not installed anywhere this suite runs: the repository carries no Pokemon
|
||||
-- Stadium data, so a clean clone has none and a developer checkout has them
|
||||
-- only after tools/stadium_pack.py has been run. What the walk below is about
|
||||
-- is the LADDER -- five rungs, in order, wrapping -- so the gate is held OPEN
|
||||
-- for it and the skipping behaviour is tested on its own further down, where
|
||||
-- it is the subject rather than an accident of the machine it ran on.
|
||||
--
|
||||
-- Held in GLOBALS rather than locals, here and for the pack probe below: this
|
||||
-- chunk is at Lua's 200-local ceiling and three more would not compile.
|
||||
BATTLE_ROW_GATE = Battles.setting.gate
|
||||
Battles.setting:setGate(function() return true end)
|
||||
|
||||
T.eq(Battles.setting:get(), true, "3D-BTL starts on 2D-3D A")
|
||||
T.eq(Battles.discs(), false, "which stages the fight on the map")
|
||||
Game.keypressed(keyGame, "8")
|
||||
@@ -1156,6 +1170,69 @@ T.eq(Battles.setting:get(), false, "again and overworld battles are off")
|
||||
Game.keypressed(keyGame, "8")
|
||||
T.eq(Battles.setting:get(), true, "and the ladder wraps back to 2D-3D A")
|
||||
|
||||
Battles.setting:setGate(BATTLE_ROW_GATE)
|
||||
|
||||
-- ------- importing a ROM instead of being told where to put one
|
||||
--
|
||||
-- The row is an ACTION, not a setting: it has no stored rung, so it is not in
|
||||
-- SETTINGS and the mod manager's page does not carry it. What it shows is a
|
||||
-- state, and what it does is open the host's file dialog -- which is why it
|
||||
-- is absent where no dialog can be opened rather than being offered as a
|
||||
-- button that does nothing.
|
||||
;(function()
|
||||
local Pick = run.loader.exports.DRAMATIC_SHAPE.lib.require("StadiumRomPick")
|
||||
local Install =
|
||||
run.loader.exports.DRAMATIC_SHAPE.lib.require("StadiumInstall")
|
||||
|
||||
T.check(type(Pick.available()) == "boolean",
|
||||
"the picker reports whether this platform has a file dialog at all")
|
||||
|
||||
local row = Pick.row()
|
||||
if not Pick.available() then
|
||||
T.eq(row, nil,
|
||||
"with no dialog available there is no row -- a button that cannot open "
|
||||
.. "anything is worse than the folder instruction it would replace")
|
||||
else
|
||||
T.check(row ~= nil, "and where there is one, there is a row")
|
||||
T.eq(row.label, "STADIUM ROM", "which says what it is for")
|
||||
T.check(type(row.step) == "function", "and does something when pressed")
|
||||
-- the value is the STATE, so a player can see whether it worked
|
||||
T.eq(row.value(), Install.available() and "READY" or "IMPORT",
|
||||
"reading READY once the models are installed and IMPORT before that")
|
||||
end
|
||||
|
||||
-- A ROM that carries no models must be refused BEFORE anything is written.
|
||||
-- An empty build otherwise completes with nothing attempted and therefore
|
||||
-- nothing failed, and the marker gets written saying so -- which on a
|
||||
-- machine that already had a set would uninstall it, because the marker is
|
||||
-- the only thing that makes 151 files on disk count as installed.
|
||||
T.eq(Install.beginFrom("", "empty"), false,
|
||||
"an empty file is refused outright")
|
||||
T.eq(select(2, Install.beginFrom(("\0"):rep(4096), "zeros")) ~= nil, true,
|
||||
"and so is a file that is not a ROM, with a reason")
|
||||
T.eq(Install.status.state ~= "building", true,
|
||||
"and neither of those started a build")
|
||||
end)()
|
||||
|
||||
-- ------- the model set, when there is one
|
||||
--
|
||||
-- Everything below that loads a .dsm needs a BUILT set, and the repository
|
||||
-- deliberately carries none: the models are Pokemon Stadium's data, built out
|
||||
-- of the player's own ROM at runtime (StadiumInstall) or by
|
||||
-- tools/stadium_pack.py into assets/stadium/ in a developer checkout. So a
|
||||
-- clean clone has nothing to read, and these say so and stand down rather
|
||||
-- than failing for the absence of data that is absent on purpose.
|
||||
--
|
||||
-- Announced rather than silent. A test that quietly evaporates when its
|
||||
-- fixture is missing is a test that has stopped running and not told anyone,
|
||||
-- which is worse than one that fails.
|
||||
HAVE_STADIUM_PACKS =
|
||||
run.loader.exports.DRAMATIC_SHAPE.lib.require("StadiumPack").available(25)
|
||||
if not HAVE_STADIUM_PACKS then
|
||||
print("SKIP stadium model tests -- no built .dsm set (run "
|
||||
.. "tools/stadium_pack.py, or play once with a ROM in baseroms/)")
|
||||
end
|
||||
|
||||
-- ------- the eyes blink once a loop, not six times a second
|
||||
--
|
||||
-- A texture animation is sampled at the SKELETAL animation's frame and HOLDS
|
||||
@@ -1222,6 +1299,7 @@ end)()
|
||||
-- Driven on a REAL species, through a rig with no meshes (there is no
|
||||
-- graphics context here, and pose() only touches the matrix arrays).
|
||||
;(function()
|
||||
if not HAVE_STADIUM_PACKS then return end
|
||||
local lib = run.loader.exports.DRAMATIC_SHAPE.lib
|
||||
local Pack, Rig = lib.require("StadiumPack"), lib.require("StadiumRig")
|
||||
local model = Pack.load(25) -- Pikachu
|
||||
@@ -1292,6 +1370,7 @@ end)()
|
||||
-- they decline the model outright and the Game Boy's own battle pic stands
|
||||
-- instead -- the same fallback a species with no pack at all takes.
|
||||
;(function()
|
||||
if not HAVE_STADIUM_PACKS then return end
|
||||
local lib = run.loader.exports.DRAMATIC_SHAPE.lib
|
||||
local Mon = lib.require("StadiumMon")
|
||||
for _, dex in ipairs({ 103, 114, 126 }) do
|
||||
@@ -1372,6 +1451,29 @@ end)()
|
||||
.. "the pic, exactly as before")
|
||||
T.eq(onField(battle, "enemy", nil), false,
|
||||
"and a side with no model at all is not held open by this")
|
||||
|
||||
-- ------- FLY and DIG take it off the field entirely
|
||||
--
|
||||
-- The charge turn runs a 19-24 frame slide and ends by setting
|
||||
-- `picFx[battler].hidden`; the release turn clears it. That field is the
|
||||
-- engine's whole answer to "is this Pokemon on screen", and it is NOT
|
||||
-- fxHidden, which is the damage blink alone -- so a model reading only the
|
||||
-- blink stood on its tile while every attack aimed at it missed.
|
||||
battle.enemy = { sprite = true }
|
||||
battle.picFx = { [battle.enemy] = { hidden = true } }
|
||||
T.eq(onField(battle, "enemy", mon("attack", false)), false,
|
||||
"a Pokemon that has flown up or dug in is not on the field, however "
|
||||
.. "much of its own animation is still to play")
|
||||
|
||||
battle.picFx = { [battle.enemy] = { kind = "slideOff", t = 4 } }
|
||||
T.eq(onField(battle, "enemy", mon("attack", false)), true,
|
||||
"but it IS while the engine's slide is still running -- which is the "
|
||||
.. "window its own launch animation plays in")
|
||||
|
||||
battle.picFx = { [battle.enemy] = {} }
|
||||
T.eq(onField(battle, "enemy", mon("idle", false)), true,
|
||||
"and a pic program that has finished and cleared leaves it standing")
|
||||
battle.picFx = nil
|
||||
end)()
|
||||
|
||||
-- ------- and the two STADIUM rungs are SKIPPED when the models are not there
|
||||
@@ -1407,9 +1509,15 @@ end)()
|
||||
Battles.setting.index = 3
|
||||
T.eq(Battles.setting:get(), true,
|
||||
"a stored STADIUM with no models behind it reads as 2D-3D A")
|
||||
Battles.setting:setGate(gate)
|
||||
-- and opening the gate hands the choice straight back, off the stored
|
||||
-- value that was never overwritten. Opened by HAND rather than by putting
|
||||
-- the real gate back: the real one answers "are the models installed on
|
||||
-- this machine", which is false wherever this suite runs from a clean
|
||||
-- clone -- and the subject here is the ladder, not the installer.
|
||||
Battles.setting:setGate(function() return true end)
|
||||
T.eq(Battles.setting:get(), "stadium",
|
||||
"and comes back the moment the models do")
|
||||
Battles.setting:setGate(gate)
|
||||
Battles.setting:setValue(true, Game)
|
||||
end)()
|
||||
|
||||
|
||||
@@ -182,6 +182,74 @@ return function(game)
|
||||
return
|
||||
end
|
||||
|
||||
-- ------- DS_FLY: does the Pokemon actually leave?
|
||||
--
|
||||
-- FLY and DIG take the user off the field for a turn, and the engine says
|
||||
-- so through `picFx[battler].hidden` rather than through fxHidden (which is
|
||||
-- the damage blink alone). A model that ignores that stands on its tile
|
||||
-- while every attack aimed at it misses into empty air.
|
||||
--
|
||||
-- A TIMELINE again, not a still: what is being checked is that the model
|
||||
-- goes when the pic goes and comes back when it comes back, which is three
|
||||
-- moments and a screenshot has one.
|
||||
if os.getenv("DS_FLY") then
|
||||
local Stadium2 = lib.require("Stadium")
|
||||
local move = os.getenv("DS_MOVE") or "FLY"
|
||||
local mine = Pokemon.new(game.data, "CHARIZARD", 60)
|
||||
-- give it the move outright: what is under test is the vanish, not
|
||||
-- whether this species learns it by level 60
|
||||
mine.moves = { { id = move, pp = 15 } }
|
||||
game.save.party = { mine }
|
||||
local battle = BattleState.newWild(game, "PIKACHU", 5)
|
||||
battle.onFinish = function() end
|
||||
game.overworld:pushBattle(battle)
|
||||
U.wait(70)
|
||||
for _ = 1, 40 do
|
||||
if battle.phase == "menu" then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
end
|
||||
U.tap(game, "a") -- FIGHT
|
||||
U.wait(12)
|
||||
U.tap(game, "a") -- the first (only) move
|
||||
|
||||
local goneAt, backAt, wasGone = nil, nil, false
|
||||
for f = 1, 400 do
|
||||
if f % 20 == 0 then U.tap(game, "a") end
|
||||
U.wait(1)
|
||||
local me = battle.player
|
||||
local pf = battle.picFx and battle.picFx[me]
|
||||
local picHidden = (pf and pf.hidden) and true or false
|
||||
local showing = Stadium2.showing("player")
|
||||
if f % 25 == 0 or (picHidden ~= wasGone) then
|
||||
U.log((" f=%3d picHidden=%-5s model=%-5s anim=%s")
|
||||
:format(f, tostring(picHidden), tostring(showing),
|
||||
tostring(Stadium2.animOf("player"))))
|
||||
end
|
||||
if picHidden and not goneAt then goneAt = f end
|
||||
if picHidden and not showing and not wasGone then
|
||||
wasGone = true
|
||||
U.shot(game, ("%s/fly_1_gone.png"):format(DIR))
|
||||
end
|
||||
if wasGone and not picHidden and not backAt then
|
||||
backAt = f
|
||||
U.shot(game, ("%s/fly_2_back.png"):format(DIR))
|
||||
break
|
||||
end
|
||||
end
|
||||
U.log(("%s: the pic hid at frame %s, the model was gone with it (%s), "
|
||||
.. "and both came back at %s")
|
||||
:format(move, tostring(goneAt), tostring(wasGone), tostring(backAt)))
|
||||
U.log(wasGone and " OK -- the model leaves the field with the pic"
|
||||
or " WRONG -- the model stood there while the game said it was gone")
|
||||
while game.stack:top() and game.stack:top() ~= game.overworld do
|
||||
game.stack:pop()
|
||||
end
|
||||
U.wait(10)
|
||||
U.log("done -- " .. DIR)
|
||||
return
|
||||
end
|
||||
|
||||
-- ------- DS_FAINT: does the collapse wait for the bar?
|
||||
--
|
||||
-- The faint animation must not start until the foe's HP bar has finished
|
||||
|
||||
Reference in New Issue
Block a user