mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-25 14:38:42 -04:00
Merge pull request #2842 from bryanthaboi/dev
This commit is contained in:
@@ -5927,7 +5927,10 @@ function BattleState:finish()
|
||||
-- the battle screen closes; leaving battle brings back the map theme,
|
||||
-- like the overworld reload's PlayDefaultMusicFadeOutCurrent
|
||||
-- (home/overworld.asm:2343-2348)
|
||||
require("src.core.Music").restoreMap(self.data)
|
||||
-- engine/battle/core.asm:928
|
||||
if not (self.result == "win" and self.musicKind == "final") then
|
||||
require("src.core.Music").restoreMap(self.data)
|
||||
end
|
||||
self.game.stack:pop()
|
||||
Runtime.emit("battle.ended", { battle = self, result = self.result or "run" })
|
||||
-- Coming back from the battle screen is a fade, not a cut: EnterMap sees
|
||||
|
||||
@@ -355,7 +355,7 @@ function FldeffMisc.hallOfFameRecord()
|
||||
-- pokeemerald/src/field_effect.c:1156
|
||||
playSe("SE_BALL")
|
||||
local o = OFFS[i] or OFFS[1]
|
||||
balls[i] = Rse().spawn("pokeball_glow", { layer = "front", x = ox + 117 + o[1] + 4, y = oy + 52 + o[2] + 4,
|
||||
balls[i] = Rse().spawn("pokeball_glow", { layer = "front", x = ox + 117 + o[1], y = oy + 52 + o[2],
|
||||
stopOnEnd = false, keep = function() return FldeffMisc._active[name] == token end })
|
||||
end
|
||||
if t == n * 25 + 32 then
|
||||
|
||||
@@ -2944,7 +2944,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
segLine("small", segs, px, ly, textW)
|
||||
elseif orderSort and mod.orderNote then
|
||||
Kit.text("small", Kit.ellipsize("small", Strings(mod.orderNote), textW),
|
||||
px, ly, PAL.yellow)
|
||||
px, ly, mod.orderNoteOptional and PAL.detail or PAL.yellow)
|
||||
elseif (mod.description or "") ~= "" then
|
||||
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
|
||||
px, ly, PAL.detail)
|
||||
@@ -5279,7 +5279,8 @@ local function buildModActionsModal(imp, m)
|
||||
if not mod then imp._modActions = nil return end
|
||||
local hasGit = mod.github and mod.github ~= ""
|
||||
local depSpecs = mod.dependencySpecs or (mod.manifest and mod.manifest.dependencySpecs)
|
||||
local hasDeps = depSpecs and #depSpecs > 0
|
||||
local optSpecs = mod.manifest and mod.manifest.optionalSpecs
|
||||
local hasDeps = (depSpecs and #depSpecs > 0) or (optSpecs and #optSpecs > 0)
|
||||
local imports = mod.imports or mod.requiredImports
|
||||
local hasImports = imports and #imports > 0
|
||||
local info = hasGit and imp:_modUpdateInfo(mod.id)
|
||||
@@ -5924,7 +5925,7 @@ local function buildDepResolverModal(imp, m)
|
||||
local n = #(res.deps or {})
|
||||
local anyUnsatisfied = false
|
||||
for _, d in ipairs(res.deps or {}) do
|
||||
if d.status ~= "satisfied" and d.status ~= "disabled" then anyUnsatisfied = true; break end
|
||||
if not d.optional and d.status ~= "satisfied" and d.status ~= "disabled" then anyUnsatisfied = true; break end
|
||||
end
|
||||
|
||||
local totalContentH = n > 0 and (n * rowH + (n - 1) * gap) or 0
|
||||
@@ -5948,7 +5949,9 @@ local function buildDepResolverModal(imp, m)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
|
||||
-- Subtitle / intro
|
||||
local subText = Strings("This mod requires additional dependencies or has conflicts:")
|
||||
local subText = res.hasIssues
|
||||
and Strings("This mod requires additional dependencies or has conflicts:")
|
||||
or Strings("Dependencies for this mod:")
|
||||
Kit.text("small", subText, px + pad, cy, PAL.muted)
|
||||
cy = cy + Kit.textHeight("small") + math.floor(10 * m.s)
|
||||
|
||||
@@ -6020,6 +6023,18 @@ local function buildDepResolverModal(imp, m)
|
||||
statusText = Strings("Missing")
|
||||
statusCol = PAL.red
|
||||
end
|
||||
if dep.kind == "optional" then
|
||||
if dep.status == "satisfied" then
|
||||
statusText = Strings("Optional: installed (v%s)", tostring(dep.installedVersion or "?"))
|
||||
elseif dep.status == "incompatible" then
|
||||
statusText = Strings("Optional: installed v%s, works with %s", tostring(dep.installedVersion or "?"), tostring(dep.range or ""))
|
||||
else
|
||||
statusText = Strings("Optional: not installed")
|
||||
end
|
||||
statusCol = dep.status == "satisfied" and PAL.green or PAL.muted
|
||||
elseif dep.kind == "dependency" then
|
||||
statusText = Strings("Required: ") .. statusText
|
||||
end
|
||||
Kit.text("micro", statusText, ix, ry + math.floor(8 * m.s) + Kit.textHeight("small") + math.floor(2 * m.s), statusCol)
|
||||
|
||||
-- Repo source line or Conflict reason
|
||||
@@ -6120,7 +6135,7 @@ local function buildDepResolverModal(imp, m)
|
||||
kind = "accent", font = "small",
|
||||
action = function()
|
||||
for _, dep in ipairs(res.deps or {}) do
|
||||
if dep.github and dep.status ~= "satisfied" and dep.status ~= "disabled" and imp._startDepPull then
|
||||
if not dep.optional and dep.github and dep.status ~= "satisfied" and dep.status ~= "disabled" and imp._startDepPull then
|
||||
imp:_startDepPull(dep)
|
||||
end
|
||||
end
|
||||
|
||||
+35
-17
@@ -114,9 +114,9 @@ end
|
||||
-- Resolves the best-known GitHub owner/repo string for a dependency spec, if any.
|
||||
function LauncherMods.resolveDependencyRepo(depId, parentManifest, installedDep)
|
||||
if not depId or depId == "" then return nil end
|
||||
-- 1. Check spec hint if parentManifest dependencySpecs carries it
|
||||
if parentManifest and parentManifest.dependencySpecs then
|
||||
for _, spec in ipairs(parentManifest.dependencySpecs) do
|
||||
for _, specs in ipairs({ parentManifest and parentManifest.dependencySpecs or {},
|
||||
parentManifest and parentManifest.optionalSpecs or {} }) do
|
||||
for _, spec in ipairs(specs) do
|
||||
if spec.id == depId and spec.github then
|
||||
return spec.github
|
||||
end
|
||||
@@ -169,10 +169,16 @@ function LauncherMods.checkDependencies(manifest, options, version, installedMan
|
||||
local depsResult = {}
|
||||
local hasIssues = false
|
||||
|
||||
-- 1. Hard Dependencies (dependencySpecs)
|
||||
if type(manifest.dependencySpecs) == "table" then
|
||||
for _, spec in ipairs(manifest.dependencySpecs) do
|
||||
if not version or ModTargets.specApplies(spec, version) then
|
||||
local depIdsSeen = {}
|
||||
for _, group in ipairs({
|
||||
{ specs = manifest.dependencySpecs, kind = "dependency" },
|
||||
{ specs = manifest.optionalSpecs, kind = "optional" },
|
||||
}) do
|
||||
for _, spec in ipairs(type(group.specs) == "table" and group.specs or {}) do
|
||||
if not depIdsSeen[spec.id]
|
||||
and (not version or ModTargets.specApplies(spec, version)) then
|
||||
depIdsSeen[spec.id] = true
|
||||
local required = group.kind == "dependency"
|
||||
local depId = spec.id
|
||||
local range = spec.range
|
||||
local installedDep = installedMap[depId]
|
||||
@@ -181,10 +187,10 @@ function LauncherMods.checkDependencies(manifest, options, version, installedMan
|
||||
|
||||
if not installedDep then
|
||||
status = "missing"
|
||||
hasIssues = true
|
||||
if required then hasIssues = true end
|
||||
elseif range and not Semver.satisfies(installedDep.version, range) then
|
||||
status = "incompatible"
|
||||
hasIssues = true
|
||||
if required then hasIssues = true end
|
||||
end
|
||||
|
||||
local ghRepo = LauncherMods.resolveDependencyRepo(depId, manifest, installedDep)
|
||||
@@ -195,7 +201,8 @@ function LauncherMods.checkDependencies(manifest, options, version, installedMan
|
||||
name = (installedDep and installedDep.name) or depId,
|
||||
range = range,
|
||||
status = status,
|
||||
kind = "dependency",
|
||||
kind = group.kind,
|
||||
optional = not required,
|
||||
installedVersion = installedVersion,
|
||||
github = ghRepo,
|
||||
safeUrl = safeUrl,
|
||||
@@ -366,20 +373,31 @@ function LauncherMods.deriveList(manifests, options, version)
|
||||
row.loadRank = rank[row.id] or (#full + 1)
|
||||
local m = byId[row.id]
|
||||
local after
|
||||
local function consider(spec)
|
||||
local seen, names = {}, { required = {}, optional = {} }
|
||||
local function consider(spec, group)
|
||||
local dep = spec and spec.id
|
||||
if dep and dep ~= row.id and rank[dep] and rank[dep] > row.loadRank
|
||||
if dep and dep ~= row.id and not seen[dep] and rank[dep]
|
||||
and rank[dep] > row.loadRank
|
||||
and ModTargets.specApplies(spec, version, generation) then
|
||||
seen[dep] = true
|
||||
local list = names[group]
|
||||
list[#list + 1] = tostring(byId[dep] and byId[dep].name or dep)
|
||||
if not after or rank[dep] > rank[after] then after = dep end
|
||||
end
|
||||
end
|
||||
for _, spec in ipairs(m.dependencySpecs or {}) do consider(spec) end
|
||||
for _, spec in ipairs(m.optionalSpecs or {}) do consider(spec) end
|
||||
for _, spec in ipairs(m.dependencySpecs or {}) do consider(spec, "required") end
|
||||
for _, spec in ipairs(m.optionalSpecs or {}) do consider(spec, "optional") end
|
||||
if after then
|
||||
local dep = byId[after]
|
||||
local parts = {}
|
||||
if #names.required > 0 then
|
||||
parts[#parts + 1] = table.concat(names.required, ", ") .. " (required)"
|
||||
end
|
||||
if #names.optional > 0 then
|
||||
parts[#parts + 1] = table.concat(names.optional, ", ") .. " (optional)"
|
||||
end
|
||||
row.orderAfter = after
|
||||
row.orderNote = "Loads after " .. tostring(dep and dep.name or after)
|
||||
.. " (dependency)"
|
||||
row.orderNote = "Loads after " .. table.concat(parts, "; ")
|
||||
row.orderNoteOptional = #names.required == 0
|
||||
end
|
||||
end
|
||||
return out
|
||||
|
||||
+41
-6
@@ -23,6 +23,11 @@ HallOfFame.isOpaque = true
|
||||
-- SGB: SetPal_PokemonWholeScreen for the mon on display
|
||||
function HallOfFame:sgbPalettes(game)
|
||||
local P = require("src.render.PaletteFX")
|
||||
if self.phase == "intro" then
|
||||
local ow = game.overworld
|
||||
if ow and ow.sgbPalettes then return ow:sgbPalettes(game) end
|
||||
return nil
|
||||
end
|
||||
if self.phase == "player" or self.phase == "player_dex"
|
||||
or self.phase == "player_rating" then
|
||||
return P.wholeNamed(game.data, "MEWMON")
|
||||
@@ -67,6 +72,13 @@ local INFO_HOLD = 80
|
||||
local HOF_HOLD = 180
|
||||
local FADE_FRAMES = 20
|
||||
|
||||
-- scripts/HallOfFame.asm:24
|
||||
local INTRO_DELAY = 3
|
||||
-- home/fade.asm:26
|
||||
local INTRO_FADE = 24
|
||||
-- engine/movie/hall_of_fame.asm:4
|
||||
local INTRO_HOLD = 100
|
||||
|
||||
-- HoFPrintTextAndDelay after each dex line
|
||||
local DEX_HOLD = 120
|
||||
|
||||
@@ -91,7 +103,10 @@ function HallOfFame.new(game, onDone)
|
||||
self.onDone = onDone
|
||||
self.index = 0
|
||||
self.timer = 0
|
||||
self.phase = "mons"
|
||||
self.phase = "intro"
|
||||
self.introT = 0
|
||||
self.isOpaque = false
|
||||
self.introFade = require("src.render.Transition").whiteOut(game)
|
||||
self.sprites = {} -- species -> image or false
|
||||
self.spriteTrueColor = {} -- species -> full-color art flag (#637)
|
||||
self.backs = {} -- species (or "@player") -> back image or false (#847)
|
||||
@@ -107,12 +122,21 @@ function HallOfFame.new(game, onDone)
|
||||
return self
|
||||
end
|
||||
|
||||
function HallOfFame:enter()
|
||||
local data = self.game.data
|
||||
if data.audio and data.audio.songs and data.audio.songs.Music_HallOfFame then
|
||||
pcall(Music.play, data, "Music_HallOfFame")
|
||||
function HallOfFame:updateIntro()
|
||||
self.introT = self.introT + 1
|
||||
if self.introT == INTRO_DELAY + 1 then
|
||||
-- engine/movie/hall_of_fame.asm:283
|
||||
Music.fadeOut(10)
|
||||
end
|
||||
if self.introT > INTRO_DELAY + INTRO_FADE then self.isOpaque = true end
|
||||
if self.introT >= INTRO_DELAY + INTRO_FADE + INTRO_HOLD then
|
||||
-- engine/movie/hall_of_fame.asm:38
|
||||
local data = self.game.data
|
||||
if data.audio and data.audio.songs and data.audio.songs.Music_HallOfFame then
|
||||
pcall(Music.play, data, "Music_HallOfFame")
|
||||
end
|
||||
self:nextMon()
|
||||
end
|
||||
self:nextMon()
|
||||
end
|
||||
|
||||
function HallOfFame:nextMon()
|
||||
@@ -194,6 +218,10 @@ function HallOfFame:update(dt)
|
||||
-- .ScrollPic with d = $a0, e = 4: the back pic crosses the screen right to
|
||||
-- left and is gone before the front pic starts. Both scrolls are plain
|
||||
-- DelayFrame loops in the ROM, so neither takes a button (#847).
|
||||
if self.phase == "intro" then
|
||||
self:updateIntro()
|
||||
return
|
||||
end
|
||||
if self.phase == "back" then
|
||||
self.scrollX = self.scrollX - SCROLL_SPEED
|
||||
if self.scrollX <= BACK_END_X then
|
||||
@@ -370,6 +398,13 @@ function HallOfFame:drawPlayerStats()
|
||||
end
|
||||
|
||||
function HallOfFame:draw()
|
||||
if self.phase == "intro" and not self.isOpaque then
|
||||
if self.introT > INTRO_DELAY then
|
||||
self.introFade.t = self.introT - INTRO_DELAY
|
||||
self.introFade:draw()
|
||||
end
|
||||
return
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
|
||||
|
||||
@@ -272,7 +272,14 @@ local function textBoxUp()
|
||||
end
|
||||
|
||||
function Player:walkPhase()
|
||||
if textBoxUp() then return 0 end
|
||||
if textBoxUp() then
|
||||
-- engine/overworld/movement.asm:49
|
||||
if not self.moving then
|
||||
self.bumpFrames = nil
|
||||
self.animClock = 0
|
||||
end
|
||||
return 0
|
||||
end
|
||||
-- moving, the land-frame after a completed step, or an active wall-bonk
|
||||
-- (issue #230) animate; a standing sprite otherwise
|
||||
if not self.moving and not self.stepLanded
|
||||
|
||||
@@ -242,6 +242,10 @@ return function(game)
|
||||
-- ---------------------------------------------------------------
|
||||
-- the back-pic sweep ahead of the first front pic
|
||||
-- ---------------------------------------------------------------
|
||||
for _ = 1, 300 do
|
||||
if hof.phase ~= "intro" then break end
|
||||
U.wait(1)
|
||||
end
|
||||
check("the induction opens on the back pic sweep, at the right edge",
|
||||
hof.phase == "back" and (hof.scrollX or 0) > 96)
|
||||
local sweepShot = false
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
-- engine/battle/core.asm:928
|
||||
-- scripts/ChampionsRoom.asm:112
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Music = require("src.core.Music")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
os.execute('mkdir -p "' .. DIR .. '" 2>/dev/null')
|
||||
|
||||
local failed = false
|
||||
local function check(label, ok)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failed = true end
|
||||
return ok
|
||||
end
|
||||
|
||||
U.newGame(game)
|
||||
local mon = Pokemon.new(game.data, "MEWTWO", 100)
|
||||
mon.moves = { { id = "SWIFT", pp = 20, maxPp = 20, ppUps = 0 } }
|
||||
game.save.party = { mon }
|
||||
game.save.player.name = "RED"
|
||||
|
||||
local realNewTrainer = BattleState.newTrainer
|
||||
local battle
|
||||
BattleState.newTrainer = function(...)
|
||||
local b = realNewTrainer(...)
|
||||
for _, m in ipairs(b.enemyParty or {}) do m.hp = 1 end
|
||||
battle = b
|
||||
return b
|
||||
end
|
||||
|
||||
U.teleport(game, "CHAMPIONS_ROOM", 4, 3, "up")
|
||||
U.wait(20)
|
||||
local ow = game.overworld
|
||||
check("overworld up in CHAMPIONS_ROOM", ow ~= nil and ow.map.id == "CHAMPIONS_ROOM")
|
||||
if not ow then love.event.quit(1) return end
|
||||
local rival
|
||||
for _, npc in ipairs(ow.npcs or {}) do
|
||||
if npc.def and npc.def.name == "CHAMPIONSROOM_RIVAL" then rival = npc end
|
||||
end
|
||||
local rows = require("data.scripts.init").get("CHAMPIONS_ROOM").talk.TEXT_CHAMPIONSROOM_RIVAL
|
||||
ow:queueScript(rows, { npc = rival })
|
||||
|
||||
for _ = 1, 3000 do
|
||||
if battle and game.stack:top() == battle then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(4)
|
||||
end
|
||||
check("rival battle on screen", battle ~= nil and game.stack:top() == battle)
|
||||
check("rival battle is the final battle", battle and battle.musicKind == "final")
|
||||
|
||||
local b = game.data.audio and game.data.audio.battle or {}
|
||||
local gymWin = b.gymWin
|
||||
for _ = 1, 4000 do
|
||||
if battle and game.stack:top() ~= battle and battle.result then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(6)
|
||||
end
|
||||
BattleState.newTrainer = realNewTrainer
|
||||
check("rival defeated", battle and battle.result == "win")
|
||||
|
||||
local cues = {}
|
||||
local realPlay = Music.play
|
||||
Music.play = function(data, song, loop, ctx)
|
||||
cues[#cues + 1] = song
|
||||
return realPlay(data, song, loop, ctx)
|
||||
end
|
||||
|
||||
U.wait(10)
|
||||
check("battle screen closed", game.stack:top() ~= battle)
|
||||
check("MUSIC_DEFEATED_GYM_LEADER still playing after the battle screen closes",
|
||||
gymWin ~= nil and Music.current() == gymWin)
|
||||
U.still(game, DIR .. "/2835_01_after_battle_victory_theme.png")
|
||||
|
||||
local sawCities, between = false, {}
|
||||
for _ = 1, 3000 do
|
||||
if Music.current() == "Music_Cities1" then sawCities = true break end
|
||||
U.tap(game, "a")
|
||||
U.wait(4)
|
||||
end
|
||||
Music.play = realPlay
|
||||
for _, s in ipairs(cues) do
|
||||
if s == "Music_Cities1" then break end
|
||||
between[#between + 1] = s
|
||||
end
|
||||
check("Oak's arrival switches to Cities1", sawCities)
|
||||
check("no map theme cue between the battle and Oak's arrival (got "
|
||||
.. table.concat(between, ",") .. ")", #between == 0)
|
||||
U.still(game, DIR .. "/2835_02_oak_arrives_cities1.png")
|
||||
|
||||
love.event.quit(failed and 1 or 0)
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
-- pokeemerald/src/field_effect.c:1066 FldEff_HallOfFameRecord
|
||||
-- pokeemerald/src/field_effect.c:597 sPokeballCoordOffsets
|
||||
local U = require("tests.drivers.util")
|
||||
local F = require("tests.drivers.em_fa_util")
|
||||
local ST = require("tests.drivers.em_story_util")
|
||||
local S = F.new("em_hof_record_balls_2838", os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_hof_record_balls_2838")
|
||||
|
||||
local EXPECT = { { 117, 52 }, { 123, 52 }, { 117, 56 }, { 123, 56 }, { 117, 60 }, { 123, 60 } }
|
||||
|
||||
return function(game)
|
||||
if not F.boot(game, S) then return S.finish() end
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Misc = require("src.core.game3.fldeff_misc")
|
||||
local Rse = require("src.core.game3.field_effects_rse")
|
||||
local session = Runtime.getSession()
|
||||
if not S.check(session.version == "emerald", "driver runs Emerald") then return S.finish() end
|
||||
|
||||
for _, sp in ipairs({ "SPECIES_SCEPTILE", "SPECIES_BLAZIKEN", "SPECIES_SWAMPERT",
|
||||
"SPECIES_GARDEVOIR", "SPECIES_AGGRON", "SPECIES_SALAMENCE" }) do
|
||||
ST.giveMon(sp, 50)
|
||||
end
|
||||
local n = 0
|
||||
for _, m in pairs(session.party or {}) do if m then n = n + 1 end end
|
||||
S.check(n == 6, "party of six (" .. n .. ")")
|
||||
|
||||
F.settle(game, 300)
|
||||
local ok, err = pcall(function()
|
||||
Map.load(nil, game, "EM_EVER_GRANDE_CITY_HALL_OF_FAME", { x = 7, y = 16, facing = "up" })
|
||||
end)
|
||||
if not ok then S.note("Map.load error: " .. tostring(err)) end
|
||||
session = Runtime.getSession()
|
||||
S.check(ok and session.map == "EM_EVER_GRANDE_CITY_HALL_OF_FAME", "warped into the Hall of Fame")
|
||||
|
||||
local started
|
||||
for _ = 1, 3000 do
|
||||
if Misc._active and Misc._active.FLDEFF_HALL_OF_FAME_RECORD then started = true break end
|
||||
if Message.isOpen and Message.isOpen() then U.tap(game, "a") end
|
||||
U.wait(1)
|
||||
end
|
||||
if not S.check(started == true, "FLDEFF_HALL_OF_FAME_RECORD started from the map script") then return S.finish() end
|
||||
S.check(Player.cellX == 7 and Player.cellY == 5 and Player.facing == "up",
|
||||
("player in front of the machine at (%s,%s) facing %s"):format(tostring(Player.cellX),
|
||||
tostring(Player.cellY), tostring(Player.facing)))
|
||||
|
||||
local function balls()
|
||||
local out = {}
|
||||
for _, e in ipairs(Rse._list) do
|
||||
if e.name == "pokeball_glow" then out[#out + 1] = e end
|
||||
end
|
||||
return out
|
||||
end
|
||||
U.wait(8)
|
||||
S.still(game, "2838_01_first_ball.png")
|
||||
for _ = 1, 200 do
|
||||
if #balls() >= 6 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(4)
|
||||
local list = balls()
|
||||
S.check(#list == 6, "six glow balls placed (" .. #list .. ")")
|
||||
S.still(game, "2838_02_six_balls_on_machine.png")
|
||||
|
||||
local ox, oy = Player.px - 112, Player.py - 72
|
||||
for i, want in ipairs(EXPECT) do
|
||||
local e = list[i]
|
||||
local sx, sy = e and (e.x - ox), e and (e.y - oy)
|
||||
S.check(e ~= nil and sx == want[1] and sy == want[2],
|
||||
("ball %d screen center (%s,%s) == pret (%d,%d)"):format(i, tostring(sx), tostring(sy), want[1], want[2]))
|
||||
if e then
|
||||
local tl = math.floor(e.x - e.sheet.fw / 2) - ox
|
||||
local tt = math.floor(e.y - e.sheet.fh / 2) - oy
|
||||
S.check(tl == want[1] - 4 and tt == want[2] - 4,
|
||||
("ball %d OAM top-left (%d,%d) == (%d,%d)"):format(i, tl, tt, want[1] - 4, want[2] - 4))
|
||||
end
|
||||
end
|
||||
|
||||
for _ = 1, 200 do
|
||||
if not (Misc._active and Misc._active.FLDEFF_HALL_OF_FAME_RECORD) then break end
|
||||
U.wait(1)
|
||||
end
|
||||
S.check(not (Misc._active and Misc._active.FLDEFF_HALL_OF_FAME_RECORD), "record effect ends")
|
||||
return S.finish()
|
||||
end
|
||||
@@ -231,6 +231,10 @@ return function(game)
|
||||
-- #847: HoFShowMonOrPlayer sweeps the BACK pic across the screen (low, at
|
||||
-- y=88) before the front pic scrolls in. Catch it mid-sweep, wait the
|
||||
-- sweep out, then catch the front pic partway through its own scroll.
|
||||
for _ = 1, 300 do
|
||||
if hof.phase ~= "intro" then break end
|
||||
U.wait(1)
|
||||
end
|
||||
for _ = 1, 300 do
|
||||
if hof.phase ~= "back" or (hof.scrollX or 0) <= 56 then break end
|
||||
U.wait(1)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
-- engine/movie/hall_of_fame.asm:1
|
||||
-- scripts/HallOfFame.asm:24
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Music = require("src.core.Music")
|
||||
local HallOfFame = require("src.ui.HallOfFame")
|
||||
os.execute('mkdir -p "' .. DIR .. '" 2>/dev/null')
|
||||
|
||||
local failed = false
|
||||
local function check(label, ok)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failed = true end
|
||||
return ok
|
||||
end
|
||||
|
||||
U.newGame(game)
|
||||
game.save.party = { Pokemon.new(game.data, "NIDOKING", 63) }
|
||||
game.save.player.name = "RED"
|
||||
game.save.pendingHallOfFame = true
|
||||
U.teleport(game, "HALL_OF_FAME", 4, 7, "up")
|
||||
|
||||
local hof
|
||||
for _ = 1, 3000 do
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) == HallOfFame then hof = top break end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
check("the induction started", hof ~= nil)
|
||||
if not hof then love.event.quit(1) return end
|
||||
|
||||
check("it opens on the fade, not the back pic sweep", hof.phase == "intro")
|
||||
check("the room is still drawn under the fade", hof.isOpaque == false)
|
||||
|
||||
local opaqueAt, lastIntro
|
||||
local midFadeShot, pauseShot, heardHof = false, false, false
|
||||
for _ = 1, 400 do
|
||||
if hof.phase ~= "intro" then break end
|
||||
lastIntro = hof.introT
|
||||
if Music.current() == "Music_HallOfFame" then heardHof = true end
|
||||
if hof.isOpaque then
|
||||
opaqueAt = opaqueAt or hof.introT
|
||||
if not pauseShot and hof.introT >= 80 then
|
||||
pauseShot = U.still(game, DIR .. "/2836_02_white_pause.png")
|
||||
end
|
||||
elseif not midFadeShot and hof.introT >= 3 + 12 then
|
||||
midFadeShot = U.still(game, DIR .. "/2836_01_fade_to_white.png")
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
local pause = (lastIntro or 0) + 1 - ((opaqueAt or 0) - 1)
|
||||
check("GBFadeOutToWhite runs after Delay3 (opaque white from intro frame "
|
||||
.. tostring(opaqueAt) .. ")", opaqueAt == 3 + 24 + 1)
|
||||
check("100 frame white pause before a mon comes in (got " .. pause .. ")",
|
||||
pause == 100)
|
||||
check("Music_HallOfFame waits for the pause", not heardHof)
|
||||
check("mid-fade screenshot", midFadeShot)
|
||||
check("white pause screenshot", pauseShot)
|
||||
check("the back pic sweep follows, entering from the right",
|
||||
hof.phase == "back" and (hof.scrollX or 0) >= 150)
|
||||
check("Music_HallOfFame starts with the sweep",
|
||||
Music.current() == "Music_HallOfFame")
|
||||
for _ = 1, 60 do
|
||||
if hof.phase ~= "back" or (hof.scrollX or 0) <= 120 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
check("back pic entering screenshot",
|
||||
U.still(game, DIR .. "/2836_03_back_pic_enters.png"))
|
||||
|
||||
love.event.quit(failed and 1 or 0)
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
-- engine/overworld/movement.asm:49
|
||||
-- home/overworld.asm:108
|
||||
-- scripts/OaksLab.asm:1011
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR")
|
||||
or "/tmp/2839"
|
||||
local fails = 0
|
||||
local function ok(cond, line)
|
||||
if not cond then fails = fails + 1 end
|
||||
print("[2839] " .. (cond and "PASS " or "FAIL ") .. line)
|
||||
end
|
||||
local function finish()
|
||||
print("[2839] " .. (fails == 0 and "ALL PASS" or (fails .. " FAILURES")))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
|
||||
local flags = game.save.flags or {}
|
||||
game.save.flags = flags
|
||||
flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
|
||||
flags.EVENT_GOT_STARTER = true
|
||||
flags.EVENT_CHOSE_SQUIRTLE = true
|
||||
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
||||
Bag.add(game.save, "OAKS_PARCEL", 1)
|
||||
game.save.objectToggles = game.save.objectToggles or {}
|
||||
game.save.objectToggles.OAKS_LAB = game.save.objectToggles.OAKS_LAB or {}
|
||||
game.save.objectToggles.OAKS_LAB.OAKSLAB_OAK1 = true
|
||||
game.save.objectToggles.OAKS_LAB.OAKSLAB_RIVAL = false
|
||||
|
||||
U.teleport(game, "OAKS_LAB", 5, 5, "down")
|
||||
U.wait(10)
|
||||
local ow = game.overworld
|
||||
local oak = ow and ow:npcByIndex(5)
|
||||
if not oak then
|
||||
ok(false, "Oak is in the lab")
|
||||
return finish()
|
||||
end
|
||||
local ox, oy = oak.cellX, oak.cellY
|
||||
U.teleport(game, "OAKS_LAB", ox - 1, oy, "right")
|
||||
U.wait(10)
|
||||
ow = game.overworld
|
||||
local p = ow.player
|
||||
ok(p.cellX == ox - 1 and p.cellY == oy, "player stands left of Oak at "
|
||||
.. (ox - 1) .. "," .. oy)
|
||||
|
||||
local sawWalk = false
|
||||
for _ = 1, 22 do
|
||||
table.insert(game.input.pressQueue, "right")
|
||||
game.input.state.right = true
|
||||
coroutine.yield()
|
||||
if p:walkPhase() == 1 then sawWalk = true end
|
||||
end
|
||||
ok(not p.moving, "Oak blocks the step")
|
||||
ok(sawWalk, "the walk-in-place cycle runs while pushing into Oak")
|
||||
local clock = p.animClock or 0
|
||||
print(string.format("[2839] bump state at talk: bumpFrames=%s animClock=%d",
|
||||
tostring(p.bumpFrames), clock))
|
||||
|
||||
U.tap(game, "a")
|
||||
game.input.state.right = false
|
||||
U.wait(4)
|
||||
ok(game.stack:top() ~= ow, "the A press opened Oak's parcel text")
|
||||
U.still(game, DIR .. "/2839_01_parcel_text.png")
|
||||
|
||||
local badFrames, worldFrames, shotWalk, shotStand = 0, 0, false, false
|
||||
for _ = 1, 3000 do
|
||||
if not ow.runner:isRunning() and game.stack:top() == ow then break end
|
||||
if game.stack:top() == ow then
|
||||
worldFrames = worldFrames + 1
|
||||
if not p.moving and p:walkPhase() ~= 0 then
|
||||
badFrames = badFrames + 1
|
||||
if not shotWalk then
|
||||
U.still(game, DIR .. "/2839_02_walk_pose_between_boxes.png")
|
||||
shotWalk = true
|
||||
end
|
||||
elseif not shotStand and worldFrames >= 3 then
|
||||
U.still(game, DIR .. "/2839_03_standing_between_boxes.png")
|
||||
shotStand = true
|
||||
end
|
||||
coroutine.yield()
|
||||
else
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
end
|
||||
end
|
||||
print(string.format("[2839] overworld frames inside the cutscene: %d, walk-pose frames: %d",
|
||||
worldFrames, badFrames))
|
||||
ok(worldFrames > 0, "the cutscene ran overworld frames between its text boxes")
|
||||
ok(badFrames == 0, "the player holds the standing frame through the parcel cutscene")
|
||||
ok(flags.EVENT_GOT_POKEDEX == true, "the cutscene ran to the Pokedex hand-off")
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
-- engine/battle/core.asm:928
|
||||
-- home/overworld.asm:2344
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Sound = require("src.core.Sound")
|
||||
local Music = require("src.core.Music")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
local realStopLoop, realRestore = Sound.stopLoop, Music.restoreMap
|
||||
local restores = 0
|
||||
Sound.stopLoop = function() end
|
||||
Music.restoreMap = function() restores = restores + 1 end
|
||||
|
||||
local function makeGame()
|
||||
local save = SaveData.newGame()
|
||||
save.party = { Pokemon.new(Data, "FIXMON_A", 20) }
|
||||
local stack = { states = {} }
|
||||
function stack:push(s) self.states[#self.states + 1] = s end
|
||||
function stack:pop() return table.remove(self.states) end
|
||||
function stack:top() return self.states[#self.states] end
|
||||
return { data = Data, save = save, stack = stack,
|
||||
input = { wasPressed = function() return false end,
|
||||
isDown = function() return false end } }
|
||||
end
|
||||
|
||||
local function finishWith(kind, result)
|
||||
local game = makeGame()
|
||||
local b = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
|
||||
game.stack:push(b)
|
||||
b.musicKind = kind
|
||||
b.result = result
|
||||
b.evolutionsChecked = true
|
||||
b.pikachuMoodChecked = true
|
||||
b.onFinish = function() end
|
||||
restores = 0
|
||||
local ok, err = pcall(b.finish, b)
|
||||
T.check(ok, kind .. "/" .. result .. ": finish runs" .. (ok and "" or (": " .. tostring(err))))
|
||||
return restores
|
||||
end
|
||||
|
||||
GameVersion.set("red")
|
||||
|
||||
T.eq(finishWith("final", "win"), 0,
|
||||
"beating RIVAL3 keeps the victory theme playing into the overworld")
|
||||
T.eq(finishWith("trainer", "win"), 1,
|
||||
"an ordinary trainer win restores the map theme")
|
||||
T.eq(finishWith("gym", "win"), 1,
|
||||
"a gym leader win restores the map theme")
|
||||
T.eq(finishWith("final", "lose"), 1,
|
||||
"losing to RIVAL3 restores the map theme")
|
||||
|
||||
Sound.stopLoop, Music.restoreMap = realStopLoop, realRestore
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,67 @@
|
||||
-- engine/movie/hall_of_fame.asm:1
|
||||
-- scripts/HallOfFame.asm:24
|
||||
-- home/fade.asm:26
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Music = require("src.core.Music")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Transition = require("src.render.Transition")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local HallOfFame = require("src.ui.HallOfFame")
|
||||
|
||||
GameVersion.set("red")
|
||||
Data.audio = Data.audio or {}
|
||||
Data.audio.songs = Data.audio.songs or {}
|
||||
Data.audio.songs.Music_HallOfFame = Data.audio.songs.Music_HallOfFame or {}
|
||||
|
||||
local realPlay, realFade = Music.play, Music.fadeOut
|
||||
local plays, fadeAt = {}, nil
|
||||
local frame = 0
|
||||
Music.play = function(_, song) plays[#plays + 1] = { song = song, frame = frame } end
|
||||
Music.fadeOut = function() fadeAt = fadeAt or frame end
|
||||
|
||||
local save = SaveData.newGame()
|
||||
save.party = { Pokemon.new(Data, "FIXMON_A", 20) }
|
||||
local game = { data = Data, save = save }
|
||||
|
||||
local hof = HallOfFame.new(game, function() end)
|
||||
T.eq(hof.phase, "intro", "induction opens on the fade, not the back pic sweep")
|
||||
T.check(not hof.isOpaque, "the map stays drawn under the fade")
|
||||
T.eq(#plays, 0, "Music_HallOfFame waits for the fade and the pause")
|
||||
|
||||
local shades = {}
|
||||
local backAt
|
||||
for f = 1, 200 do
|
||||
frame = f
|
||||
hof:update(1 / 60)
|
||||
if hof.phase == "intro" then
|
||||
PaletteFX.setShadeMap(nil)
|
||||
hof:draw()
|
||||
shades[f] = PaletteFX.shadeMap()
|
||||
elseif not backAt then
|
||||
backAt = f
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
T.eq(fadeAt, 4, "HoFFadeOutScreenAndMusic fades the music after Delay3")
|
||||
T.eq(shades[3], nil, "Delay3 leaves the palette alone")
|
||||
T.eq(shades[4], Transition.shadeMapFor(0x90), "fade step 1 on frame 4")
|
||||
T.eq(shades[11], Transition.shadeMapFor(0x90), "fade step 1 holds 8 frames")
|
||||
T.eq(shades[12], Transition.shadeMapFor(0x40), "fade step 2 on frame 12")
|
||||
T.eq(shades[20], Transition.shadeMapFor(0x00), "fade step 3 on frame 20")
|
||||
T.check(hof.isOpaque, "the screen is cleared to white after the fade")
|
||||
T.eq(backAt, 3 + 24 + 100, "100 DelayFrames of white before the first mon")
|
||||
T.eq(hof.phase, "back", "the back pic sweep starts after the pause")
|
||||
T.eq(hof.scrollX, 160, "the back pic enters from the right edge")
|
||||
T.eq(plays[1] and plays[1].song, "Music_HallOfFame", "hall of fame theme starts")
|
||||
T.eq(plays[1] and plays[1].frame, backAt, "theme starts with the sweep, after the pause")
|
||||
|
||||
Music.play, Music.fadeOut = realPlay, realFade
|
||||
PaletteFX.setShadeMap(nil)
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,74 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local Manifest = require("src.mods.Manifest")
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
|
||||
local function mf(raw) return Manifest.validate(raw) end
|
||||
|
||||
local engine = mf({ id = "g9-battle-engine", name = "G9 Battle Engine",
|
||||
version = "1.0.0", entry = "m.lua" })
|
||||
local scene = mf({ id = "g9-Battle-Scene", name = "G9 Battle Scene",
|
||||
version = "1.0.0", entry = "m.lua", dependencies = { "g9-battle-engine" } })
|
||||
local sprites = mf({ id = "g9-battle-sprites", name = "G9 Battle Sprites",
|
||||
version = "1.0.0", entry = "m.lua",
|
||||
optional_dependencies = { "national_dex", "g9-battle-engine", "g9-Battle-Scene" } })
|
||||
local dex = mf({ id = "national_dex", name = "National Dex",
|
||||
version = "1.0.0", entry = "m.lua" })
|
||||
|
||||
do
|
||||
local res = LauncherMods.checkDependencies(sprites, nil, nil, { sprites })
|
||||
T.eq(res.hasIssues, false, "optional deps alone never raise the resolver")
|
||||
T.eq(#res.deps, 3, "resolver lists every optional dependency")
|
||||
for _, d in ipairs(res.deps) do
|
||||
T.eq(d.kind, "optional", d.id .. " is marked optional")
|
||||
T.eq(d.status, "missing", d.id .. " reports not installed")
|
||||
end
|
||||
|
||||
local partial = LauncherMods.checkDependencies(sprites, nil, nil, { sprites, scene })
|
||||
T.eq(partial.hasIssues, false, "installed optional dep does not flag the rest")
|
||||
local status = {}
|
||||
for _, d in ipairs(partial.deps) do status[d.id] = d.status end
|
||||
T.eq(status["g9-Battle-Scene"], "satisfied", "installed optional shows satisfied")
|
||||
T.eq(status["national_dex"], "missing", "absent optional still listed")
|
||||
|
||||
local hard = LauncherMods.checkDependencies(scene, nil, nil, { scene })
|
||||
T.eq(hard.hasIssues, true, "a missing hard dependency still raises the resolver")
|
||||
T.eq(hard.deps[1].kind, "dependency", "hard dependency kind kept")
|
||||
end
|
||||
|
||||
do
|
||||
local manifests = { engine, scene, sprites, dex }
|
||||
local function noteFor(order)
|
||||
local rows = LauncherMods.deriveList(manifests, { mods = {}, modOrder = order }, "crystal")
|
||||
for _, r in ipairs(rows) do
|
||||
if r.id == "g9-battle-sprites" then return r end
|
||||
end
|
||||
end
|
||||
local a = noteFor({ "g9-battle-sprites", "national_dex", "g9-battle-engine", "g9-Battle-Scene" })
|
||||
T.check(a.orderNote and a.orderNote:find("(optional)", 1, true) ~= nil,
|
||||
"optional ordering note says optional: " .. tostring(a.orderNote))
|
||||
T.check(a.orderNote and not a.orderNote:find("dependency", 1, true),
|
||||
"optional ordering note never calls it a dependency")
|
||||
T.eq(a.orderNoteOptional, true, "optional-only note is not a warning")
|
||||
for _, name in ipairs({ "National Dex", "G9 Battle Engine", "G9 Battle Scene" }) do
|
||||
T.check(a.orderNote:find(name, 1, true) ~= nil, "note names " .. name)
|
||||
end
|
||||
|
||||
local b = noteFor({ "g9-battle-sprites", "g9-Battle-Scene", "g9-battle-engine", "national_dex" })
|
||||
T.eq(b.orderNote, a.orderNote, "note text is stable when the later deps reorder")
|
||||
|
||||
local scenes = LauncherMods.deriveList(manifests,
|
||||
{ mods = {}, modOrder = { "g9-Battle-Scene", "g9-battle-engine" } }, "crystal")
|
||||
for _, r in ipairs(scenes) do
|
||||
if r.id == "g9-Battle-Scene" then
|
||||
T.check(r.orderNote and r.orderNote:find("(required)", 1, true) ~= nil,
|
||||
"hard dependency note says required")
|
||||
T.check(not r.orderNoteOptional, "hard dependency note stays a warning")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
T.finish("mod optional dependencies (#2833)")
|
||||
@@ -114,7 +114,14 @@ check(getmetatable(stack2:top()) == HallOfFame, "induction showcase pushed")
|
||||
-- $c0 -> $a0) and only then scrolls the front pic in (#847), so the
|
||||
-- induction opens on the back pass.
|
||||
local hofUi = stack2:top()
|
||||
eq(hofUi.phase, "back", "induction opens on the back pic sweep (#847)")
|
||||
eq(hofUi.phase, "intro", "induction opens on the fade to white")
|
||||
local introGuard = 0
|
||||
while hofUi.phase == "intro" and introGuard < 400 do
|
||||
introGuard = introGuard + 1
|
||||
hofUi:update(1 / 60)
|
||||
end
|
||||
eq(introGuard, 3 + 24 + 100, "Delay3 + GBFadeOutToWhite + 100 DelayFrames before the sweep")
|
||||
eq(hofUi.phase, "back", "the back pic sweep follows the fade (#847)")
|
||||
eq(hofUi.scrollX, 160, "back pic enters at the right edge (hSCX = $c0)")
|
||||
-- drive the back sweep and the front scroll so the info box is armed
|
||||
local scrollGuard = 0
|
||||
|
||||
Reference in New Issue
Block a user