mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
title screen issues, audio issues, and replacing gf c
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
-- ..(engine/movie/title.asm ln 28)
|
||||
-- ..(engine/movie/title2.asm ln 13)
|
||||
-- POKEPORT_DRIVER=tests/drivers/title_cycle_test.lua POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local shot = 0
|
||||
local function grab(tag)
|
||||
shot = shot + 1
|
||||
U.shot(game, ("%s/title_%02d_%s.png"):format(DIR, shot, tag))
|
||||
end
|
||||
|
||||
U.wait(30)
|
||||
grab("copyright")
|
||||
-- ..(engine/movie/splash.asm ln 230)
|
||||
local movie = game.stack:top()
|
||||
while movie.phase ~= 2 or movie.timer < 70 do U.wait(1) end
|
||||
grab("star_topbar")
|
||||
while movie.timer < 88 do U.wait(1) end
|
||||
grab("star_middle")
|
||||
while movie.timer < 100 do U.wait(1) end
|
||||
grab("star_lowbar")
|
||||
while movie.timer < 130 do U.wait(1) end
|
||||
grab("gamefreak")
|
||||
|
||||
U.tap(game, "start")
|
||||
U.wait(2)
|
||||
local title = game.stack:top()
|
||||
U.log("top is", tostring(title and title.screenId))
|
||||
if not (title and title.scrollPhase) then
|
||||
U.log("no TitleState on top; nothing below can run")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
grab("drop_early")
|
||||
U.wait(14)
|
||||
grab("drop_late")
|
||||
while title.phase == "drop" do U.wait(1) end
|
||||
grab("settle")
|
||||
while title.phase == "settle" do U.wait(1) end
|
||||
grab("ribbon_start")
|
||||
U.wait(10)
|
||||
U.shot(game, DIR .. "/title_ribbon_mid.png")
|
||||
while title.phase ~= "loop" do U.wait(1) end
|
||||
grab("landed")
|
||||
|
||||
title.cycleIndex = 1
|
||||
title.scrollPhase, title.scrollFrame, title.timer = "hold", 1, 0
|
||||
title.monOffset = 0
|
||||
while title.scrollPhase == "hold" do U.wait(1) end
|
||||
grab("out_a")
|
||||
U.wait(6)
|
||||
grab("out_b")
|
||||
while title.scrollPhase == "out" do U.wait(1) end
|
||||
U.log("after the scroll out the phase is", title.scrollPhase)
|
||||
for _ = 1, 5 do
|
||||
grab("ball")
|
||||
U.wait(1)
|
||||
end
|
||||
while title.scrollPhase == "ball" do U.wait(1) end
|
||||
grab("in")
|
||||
U.wait(30)
|
||||
grab("next_mon")
|
||||
U.log("captured", DIR)
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -65,8 +65,12 @@ function U.newGame(game)
|
||||
U.wait(5)
|
||||
U.tap(game, "start") -- skip intro movie
|
||||
U.wait(10)
|
||||
U.tap(game, "a") -- title -> menu
|
||||
U.wait(5)
|
||||
local title = game.stack:top()
|
||||
for _ = 1, 60 do
|
||||
U.tap(game, "a")
|
||||
U.wait(5)
|
||||
if game.stack:top() ~= title then break end
|
||||
end
|
||||
-- menu: CONTINUE may or may not exist; NEW GAME is first without a save
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
local eq = T.eq
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local ChipAsm = require("src.audio.ChipAsm")
|
||||
local ChipSynth = require("src.core.ChipSynth")
|
||||
|
||||
local data = { audio = {} }
|
||||
|
||||
local function pulseSong()
|
||||
return ChipAsm.song{
|
||||
channels = { { hw = 1, program = {
|
||||
{ duty = 2 },
|
||||
{ notetype = { speed = 12, volume = 15, fade = 0 } },
|
||||
{ octave = 4 },
|
||||
{ note = "C", len = 15 },
|
||||
} } },
|
||||
}
|
||||
end
|
||||
|
||||
local function noiseSong()
|
||||
return ChipAsm.sfx{
|
||||
channels = { { hw = 4, program = {
|
||||
{ noiseNote = { len = 8, volume = 15, fade = 1, parameter = 0x34 } },
|
||||
} } },
|
||||
}
|
||||
end
|
||||
|
||||
do
|
||||
local engine = ChipSynth.newEngine(data, pulseSong(), { allowLoops = false })
|
||||
local sawNeg, sawPos = false, false
|
||||
for _ = 1, 512 do
|
||||
local v = engine.channels[1]:sample()
|
||||
if v < -1e-12 then sawNeg = true end
|
||||
if v > 1e-12 then sawPos = true end
|
||||
end
|
||||
check(sawPos and not sawNeg,
|
||||
"pulse DAC is unipolar (high = volume, low = 0)")
|
||||
end
|
||||
|
||||
do
|
||||
local engine = ChipSynth.newEngine(data, noiseSong(), {
|
||||
sfx = true, allowLoops = false,
|
||||
})
|
||||
local sawNeg, sawPos = false, false
|
||||
for _ = 1, 2048 do
|
||||
local v = engine.channels[1]:sample()
|
||||
if v < -1e-12 then sawNeg = true end
|
||||
if v > 1e-12 then sawPos = true end
|
||||
end
|
||||
check(sawPos and not sawNeg,
|
||||
"noise DAC is unipolar (LFSR high = volume, low = 0)")
|
||||
end
|
||||
|
||||
local function crossingsAndSign(engine, frames)
|
||||
local count, prev = 0, nil
|
||||
local sawNeg, sawPos = false, false
|
||||
for _ = 1, frames do
|
||||
local sample = engine:sample()
|
||||
if sample < -1e-12 then sawNeg = true end
|
||||
if sample > 1e-12 then sawPos = true end
|
||||
if prev and prev * sample < 0 then count = count + 1 end
|
||||
prev = sample
|
||||
end
|
||||
return count, sawNeg, sawPos
|
||||
end
|
||||
|
||||
do
|
||||
local engine = ChipSynth.newEngine(data, pulseSong(), { allowLoops = false })
|
||||
local count, sawNeg, sawPos = crossingsAndSign(engine, 4000)
|
||||
check(sawNeg and sawPos, "HPF centers a unipolar pulse around analog 0")
|
||||
check(count > 20, ("HPF'd pulse crosses zero (%d crossings)"):format(count))
|
||||
end
|
||||
|
||||
do
|
||||
local engine = ChipSynth.newEngine(data, noiseSong(), {
|
||||
sfx = true, allowLoops = false,
|
||||
})
|
||||
local count, sawNeg, sawPos = crossingsAndSign(engine, 8000)
|
||||
check(sawNeg and sawPos, "HPF centers noise / drums around analog 0")
|
||||
check(count > 50, ("HPF'd noise crosses zero (%d crossings)"):format(count))
|
||||
end
|
||||
|
||||
do
|
||||
local song = pulseSong()
|
||||
ChipSynth.setChannelVolumes({ 1, 1, 1, 1 })
|
||||
local a = ChipSynth.newEngine(data, song, { allowLoops = false })
|
||||
local base = a.channels[1]:sample()
|
||||
ChipSynth.setChannelVolume(1, 0.25)
|
||||
local b = ChipSynth.newEngine(data, song, { allowLoops = false })
|
||||
local quarter = b.channels[1]:sample()
|
||||
ChipSynth.setChannelVolumes({ 1, 1, 1, 1 })
|
||||
check(base > 0 and math.abs(quarter - base * 0.25) < 1e-9,
|
||||
"channelVolume still quarters the unipolar DAC level")
|
||||
end
|
||||
|
||||
eq(type(ChipSynth.newEngine), "function", "engine factory still exported")
|
||||
|
||||
T.finish("chip analog path")
|
||||
@@ -0,0 +1,82 @@
|
||||
-- ..(audio/engine_1.asm ln 197)
|
||||
-- ..(audio/sfx/noise_instrument01_1.asm ln 1)
|
||||
-- luajit tests/engine/drum_envelope_ring.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local ChipAsm = require("src.audio.ChipAsm")
|
||||
local ChipSynth = require("src.core.ChipSynth")
|
||||
|
||||
local snare = ChipAsm.song{
|
||||
channels = {
|
||||
{ hw = 4, program = {
|
||||
{ notetype = { speed = 12 } },
|
||||
{ drum = 1, len = 2 },
|
||||
{ rest = 16 },
|
||||
} },
|
||||
},
|
||||
drums = {
|
||||
[1] = {
|
||||
{ len = 1, volume = 12, fade = 1, parameter = 0x33 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local engine = ChipSynth.newEngine({ audio = {} }, snare, { allowLoops = false })
|
||||
local segs = engine:noiseInstrument(1)
|
||||
local last = segs[#segs]
|
||||
local ringMs = (last.endSample - last.startSample) / ChipSynth.SAMPLE_RATE * 1000
|
||||
check(ringMs > 150 and ringMs < 220,
|
||||
("snare instrument rings ~188ms, not the 17ms note (%0.1fms)"):format(ringMs))
|
||||
|
||||
local energyEarly, energyLate, energyEnd = 0, 0, 0
|
||||
local total = math.floor(ChipSynth.SAMPLE_RATE * 0.25)
|
||||
for i = 1, total do
|
||||
local s = engine:sample()
|
||||
local e = s * s
|
||||
local t = i / ChipSynth.SAMPLE_RATE
|
||||
if t < 0.02 then
|
||||
energyEarly = energyEarly + e
|
||||
elseif t > 0.05 and t < 0.12 then
|
||||
energyLate = energyLate + e
|
||||
elseif t > 0.20 then
|
||||
energyEnd = energyEnd + e
|
||||
end
|
||||
end
|
||||
check(energyEarly > 0, "snare attack is audible")
|
||||
check(energyLate > energyEarly * 0.05,
|
||||
("snare body still sounds at 50-120ms (early=%.4f late=%.4f)")
|
||||
:format(energyEarly, energyLate))
|
||||
check(energyEnd < energyLate * 0.1,
|
||||
"snare has decayed by 200ms")
|
||||
|
||||
local hats = ChipAsm.song{
|
||||
channels = {
|
||||
{ hw = 4, program = {
|
||||
{ notetype = { speed = 12 } },
|
||||
{ drum = 1, len = 2 },
|
||||
{ drum = 1, len = 2 },
|
||||
} },
|
||||
},
|
||||
drums = {
|
||||
[1] = {
|
||||
{ len = 1, volume = 8, fade = 1, parameter = 0x10 },
|
||||
},
|
||||
},
|
||||
}
|
||||
local hatEngine = ChipSynth.newEngine({ audio = {} }, hats, { allowLoops = false })
|
||||
local hits = 0
|
||||
local prev = 0
|
||||
for _ = 1, math.floor(ChipSynth.SAMPLE_RATE * 0.3) do
|
||||
local s = math.abs(hatEngine:sample())
|
||||
if prev < 0.01 and s >= 0.01 then hits = hits + 1 end
|
||||
prev = s
|
||||
end
|
||||
check(hits >= 2, ("two rapid drum_notes both trigger (%d onsets)"):format(hits))
|
||||
|
||||
T.finish("drum envelope ring")
|
||||
@@ -0,0 +1,94 @@
|
||||
-- ..(home/audio.asm ln 9)
|
||||
-- ..(home/fade_audio.asm ln 36)
|
||||
-- luajit tests/engine/map_music_fade.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
local eq = T.eq
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local Source = {}
|
||||
Source.__index = Source
|
||||
function Source:play() self.playing = true end
|
||||
function Source:stop() self.playing = false end
|
||||
function Source:pause() self.playing = false end
|
||||
function Source:isPlaying() return self.playing end
|
||||
function Source:setLooping() end
|
||||
function Source:setVolume(v) self.volume = v end
|
||||
function Source:setPitch() end
|
||||
function Source:setFilter() end
|
||||
function Source:getDuration() return 1 end
|
||||
|
||||
local made = {} -- file -> the last source built for it
|
||||
love.audio = {
|
||||
newSource = function(file, mode)
|
||||
made[file] = setmetatable({ file = file, mode = mode }, Source)
|
||||
return made[file]
|
||||
end,
|
||||
}
|
||||
|
||||
local Music = require("src.core.Music")
|
||||
|
||||
local data = { audio = {
|
||||
songs = {
|
||||
Music_Pallet = { file = "pallet.wav" },
|
||||
Music_Routes1 = { file = "routes1.wav" },
|
||||
Music_Pewter = { file = "pewter.wav" },
|
||||
},
|
||||
mapSongs = {
|
||||
PALLET_TOWN = "Music_Pallet",
|
||||
ROUTE_1 = "Music_Routes1",
|
||||
PEWTER_CITY = "Music_Pewter",
|
||||
},
|
||||
} }
|
||||
|
||||
local function frames(n)
|
||||
for _ = 1, n do Music.update(data) end
|
||||
end
|
||||
|
||||
local function playing()
|
||||
for file, src in pairs(made) do
|
||||
if src.playing then return file end
|
||||
end
|
||||
return "(silence)"
|
||||
end
|
||||
|
||||
local FADE = 7 * Music.MAP_FADE -- 7 volume levels x 10 frames
|
||||
|
||||
Music.stop()
|
||||
Music.playMap(data, "PALLET_TOWN", false, false, Music.MAP_FADE)
|
||||
eq(playing(), "pallet.wav", "the first map after boot starts at once")
|
||||
|
||||
local fullVolume = made["pallet.wav"].volume
|
||||
|
||||
Music.playMap(data, "ROUTE_1", false, false, Music.MAP_FADE)
|
||||
eq(playing(), "pallet.wav", "the new theme waits while the old one fades")
|
||||
frames(FADE - 1)
|
||||
eq(playing(), "pallet.wav", "still fading one frame short of silence")
|
||||
check(made["pallet.wav"].volume < fullVolume,
|
||||
"the old theme has been ramped down by then")
|
||||
frames(1)
|
||||
eq(playing(), "routes1.wav", "the queued theme takes over after 7 * 10 frames")
|
||||
|
||||
eq(made["routes1.wav"].volume, fullVolume,
|
||||
"the new theme starts at full volume, not where the ramp ended")
|
||||
|
||||
Music.playMap(data, "ROUTE_1", false, false, Music.MAP_FADE)
|
||||
eq(playing(), "routes1.wav", "the same theme keeps playing")
|
||||
frames(FADE)
|
||||
eq(playing(), "routes1.wav", "and no fade was armed for it")
|
||||
|
||||
Music.playMap(data, "PALLET_TOWN", false, false, Music.MAP_FADE)
|
||||
frames(3 * Music.MAP_FADE)
|
||||
Music.playMap(data, "PEWTER_CITY", false, false, Music.MAP_FADE)
|
||||
eq(playing(), "routes1.wav", "the retargeted fade keeps ramping the old theme")
|
||||
frames(4 * Music.MAP_FADE)
|
||||
eq(playing(), "pewter.wav", "the ramp lands on the newest map's theme")
|
||||
|
||||
Music.playMap(data, "PALLET_TOWN", false, false)
|
||||
eq(playing(), "pallet.wav", "a fadeless map cue swaps immediately")
|
||||
|
||||
T.finish("map_music_fade")
|
||||
@@ -37,7 +37,7 @@ end
|
||||
|
||||
local Y_TITLE = {
|
||||
"pikachu.png", "pika_bubble.png", "eyes_half.png", "eyes_closed.png",
|
||||
"player.png", "copyright.png", "yellow_version.png",
|
||||
"player.png", "copyright.png", "gamefreak_inc.png", "yellow_version.png",
|
||||
}
|
||||
for _, name in ipairs(Y_TITLE) do
|
||||
seed("yellow/assets/generated/title/" .. name)
|
||||
@@ -266,6 +266,7 @@ GameVersion.set("blue")
|
||||
seed("blue/assets/generated/title/blue_version.png", "blue-version-bytes")
|
||||
seed("blue/assets/generated/title/player.png")
|
||||
seed("blue/assets/generated/title/copyright.png")
|
||||
seed("blue/assets/generated/title/gamefreak_inc.png")
|
||||
local B_INTRO = {
|
||||
"gf_logo.png", "gf_text.png", "big_star.png",
|
||||
"falling_star.png", "falling_star_blink.png", "studio_logo.png",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
-- ..(engine/movie/title.asm ln 227)
|
||||
-- ..(engine/movie/title2.asm ln 13)
|
||||
-- luajit tests/engine/title_mon_cycle.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
local eq = T.eq
|
||||
|
||||
love = require("tests.love_stub")
|
||||
love.math = love.math or {}
|
||||
local nextPick = 1
|
||||
love.math.random = function(lo, hi)
|
||||
nextPick = nextPick % hi + 1
|
||||
return math.max(lo, nextPick)
|
||||
end
|
||||
|
||||
local TitleState = require("src.ui.TitleState")
|
||||
|
||||
local title = TitleState.new(
|
||||
{ data = {}, input = { wasPressed = function() return false end } }, {})
|
||||
title.sprites = setmetatable({}, { __index = function() return false end })
|
||||
|
||||
eq(title.phase, "drop", "Red/Blue boot into the logo drop, not the loop")
|
||||
local ribbonSeen = {}
|
||||
for _ = 1, 400 do
|
||||
if title.phase == "loop" then break end
|
||||
title:update(1 / 60)
|
||||
if title.phase == "ribbon" then
|
||||
ribbonSeen[#ribbonSeen + 1] = title.ribbonOffset
|
||||
end
|
||||
end
|
||||
eq(title.phase, "loop", "the cinematic lands within 400 frames")
|
||||
eq(ribbonSeen[1], 112,
|
||||
"the ribbon is parked off the right edge on its first drawn frame")
|
||||
eq(ribbonSeen[#ribbonSeen], 4, "and walks in 4px a frame to its rest")
|
||||
|
||||
title.cycleIndex = 1 -- CHARMANDER: a starter, so the ball juggle runs
|
||||
title.scrollPhase, title.scrollFrame, title.timer = "hold", 1, 0
|
||||
title.monOffset = 0
|
||||
|
||||
local frames = {}
|
||||
for _ = 1, 260 do
|
||||
title:update(1 / 60)
|
||||
frames[#frames + 1] = {
|
||||
phase = title.scrollPhase, offset = title.monOffset,
|
||||
ball = title.ballY, mon = title.cycleSpecies[title.cycleIndex],
|
||||
}
|
||||
end
|
||||
|
||||
local HOLD_FRAMES = 200
|
||||
|
||||
local function span(phase)
|
||||
local first, count = nil, 0
|
||||
for i, f in ipairs(frames) do
|
||||
if f.phase == phase then
|
||||
if not first then first = i end
|
||||
if first + count == i then count = count + 1 end
|
||||
end
|
||||
end
|
||||
return first, count
|
||||
end
|
||||
|
||||
local holdAt, holdLen = span("hold")
|
||||
local outAt, outLen = span("out")
|
||||
local ballAt, ballLen = span("ball")
|
||||
local inAt, inLen = span("in")
|
||||
eq(holdAt, 1, "the cycle opens on the hold")
|
||||
eq(holdLen, HOLD_FRAMES - 1, "ld c, 200 / CheckForUserInterruption")
|
||||
eq(outAt, HOLD_FRAMES, "the scroll out begins as the 200th hold frame ends")
|
||||
eq(outLen, 18, "TitleScroll_Out is 2+2+2+2+2+2+3+3 frames")
|
||||
eq(ballLen, 10, "TitleScroll_WaitBall is two runs of 5")
|
||||
eq(inLen, 17, "TitleScroll_In is 2+4+4+3+2+1+1 frames")
|
||||
check(outAt < ballAt and ballAt < inAt, "out, then the ball, then in")
|
||||
|
||||
local OUT = { 0, -1, -2, -4, -6, -9, -12, -16, -20, -25, -30, -36, -42,
|
||||
-50, -58, -66, -75, -84 }
|
||||
for i, want in ipairs(OUT) do
|
||||
eq(frames[outAt + i - 1].offset, want,
|
||||
"TitleScroll_Out offset at frame " .. i)
|
||||
end
|
||||
|
||||
local IN = { 120, 110, 100, 91, 82, 73, 64, 56, 48, 40, 32, 26, 20, 14, 9,
|
||||
4, 1 }
|
||||
for i, want in ipairs(IN) do
|
||||
eq(frames[inAt + i - 1].offset, want, "TitleScroll_In offset at frame " .. i)
|
||||
end
|
||||
|
||||
local BALL = { 97, 95, 94, 93, 92, 93, 94, 95, 97, 100 }
|
||||
for i, want in ipairs(BALL) do
|
||||
eq(frames[ballAt + i - 1].ball, want, "TitleBallYTable entry " .. i)
|
||||
end
|
||||
|
||||
local outgoing = frames[outAt].mon
|
||||
eq(outgoing, "CHARMANDER", "the starter is the one that scrolls out")
|
||||
for i = outAt, inAt - 1 do
|
||||
eq(frames[i].mon, outgoing,
|
||||
"the pick does not change before the scroll in, at frame " .. i)
|
||||
end
|
||||
local incoming = frames[inAt].mon
|
||||
check(incoming ~= outgoing, "TitleScreenPickNewMon never repeats the pick")
|
||||
for i = inAt, inAt + inLen - 1 do
|
||||
check(frames[i].offset > 0,
|
||||
"the incoming mon is only ever drawn right of rest, at frame " .. i)
|
||||
end
|
||||
eq(frames[inAt + inLen].offset, 0, "and settles at its resting column")
|
||||
|
||||
T.finish("title_mon_cycle")
|
||||
@@ -93,6 +93,7 @@ local titleGame = {
|
||||
field = { title = { cycleSpecies = { "PIKACHU" } } } },
|
||||
}
|
||||
local title = TitleState.new(titleGame, {})
|
||||
title.phase, title.scy = "loop", 0
|
||||
local titleSprite, titleTrueColor = title:currentSprite()
|
||||
check(titleSprite and titleTrueColor,
|
||||
"title cache keeps a Pokemon sprite's trueColor flag")
|
||||
|
||||
Reference in New Issue
Block a user