mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
Merge pull request #1541 from bryanthaboi/dev
tuesday afternoon squashing
This commit is contained in:
@@ -234,7 +234,11 @@ local imageMeta = setmetatable({}, WEAK_KEYS)
|
||||
-- entirely, so its palette variant collapses back onto the plain path.
|
||||
local function getImage(path, pal, trueColor)
|
||||
if not path then return nil end
|
||||
if trueColor then pal = nil end
|
||||
if trueColor and require("src.render.PaletteFX").honorsTrueColor() then
|
||||
pal = nil
|
||||
else
|
||||
trueColor = nil
|
||||
end
|
||||
local key = pal and (path .. "#" .. pal.name) or path
|
||||
if not imageCache[key] then
|
||||
local img, pad, padL = nil, 0, 0
|
||||
|
||||
@@ -78,17 +78,31 @@ end
|
||||
|
||||
-- Fishing: a rod's list is (cumulative chance, species, level) rows out of 256,
|
||||
-- ending at 100%. A roll past the group's own `chance` is a bite of nothing.
|
||||
function Encounter.fish(encounters, fishGroup, rod, random)
|
||||
-- Rows with `day` and `nite` sub-slots (from TimeFishGroups) resolve based on
|
||||
-- `daytime` ("MORN"/"DAY" vs "NITE"/"DARK").
|
||||
function Encounter.fish(encounters, fishGroup, rod, daytime, random)
|
||||
if type(daytime) == "function" and random == nil then
|
||||
random = daytime
|
||||
daytime = nil
|
||||
end
|
||||
local group = encounters and encounters.fishGroups
|
||||
and encounters.fishGroups[fishGroup]
|
||||
if not group then return nil end
|
||||
local list = group[rod or "old"]
|
||||
if not list or #list == 0 then return nil end
|
||||
local value = roll(random, 256)
|
||||
local isNight = (daytime == "DARK" or daytime == "NITE")
|
||||
local todKey = isNight and "nite" or "day"
|
||||
for _, row in ipairs(list) do
|
||||
if value < (row.chance or 0) then
|
||||
if not row.species or row.species == "NO_ITEM" then return nil end
|
||||
return { species = row.species, level = row.level }
|
||||
local slot = row[todKey]
|
||||
if not slot and row.timeGroup and encounters and encounters.timeFishGroups then
|
||||
local tg = encounters.timeFishGroups[row.timeGroup]
|
||||
slot = tg and tg[todKey]
|
||||
end
|
||||
slot = slot or row
|
||||
if not slot.species or slot.species == 0 or slot.species == "NO_ITEM" then return nil end
|
||||
return { species = slot.species, level = slot.level }
|
||||
end
|
||||
end
|
||||
return nil
|
||||
@@ -129,7 +143,7 @@ end
|
||||
|
||||
-- Which fish group a MAP belongs to lives on the map record, so a caller with
|
||||
-- a map id and a rod does not have to know about groups at all.
|
||||
function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm)
|
||||
function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm, daytime)
|
||||
local map = maps and maps[mapId]
|
||||
local group = map and map.fishGroup
|
||||
if not group then
|
||||
@@ -142,7 +156,7 @@ function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm)
|
||||
if rod == "OLD_ROD" then key = "old"
|
||||
elseif rod == "GOOD_ROD" then key = "good"
|
||||
elseif rod == "SUPER_ROD" then key = "super" end
|
||||
return Encounter.fish(encounters, group, key or "old", random)
|
||||
return Encounter.fish(encounters, group, key or "old", daytime, random)
|
||||
end
|
||||
|
||||
-- Headbutt trees: TreeMonMaps says which set a map uses and TreeMons holds
|
||||
|
||||
+95
-11
@@ -66,6 +66,8 @@ local pendingBuf -- a current-gen buffer popped from the worker but not yet
|
||||
-- the jingle ends.
|
||||
local musicHeld = false
|
||||
|
||||
local suspended = false
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- worker management
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -150,12 +152,15 @@ local MUSIC_FILL_INITIAL = 4
|
||||
local MUSIC_FILL_PER_CALL = 3
|
||||
|
||||
local function fillSync(limit)
|
||||
if suspended then return end
|
||||
local music = currentMusic
|
||||
if not music or not music.engine or music.engine:finished() then return end
|
||||
limit = limit or MUSIC_FILL_PER_CALL
|
||||
local free = music.source:getFreeBufferCount()
|
||||
local ok, free = pcall(music.source.getFreeBufferCount, music.source)
|
||||
if not ok or type(free) ~= "number" then return end
|
||||
while free > 0 and limit > 0 and not music.engine:finished() do
|
||||
music.source:queue(ChipSynth.soundData(music.engine, MUSIC_BUFFER_SAMPLES, 2))
|
||||
local sd = ChipSynth.soundData(music.engine, MUSIC_BUFFER_SAMPLES, 2)
|
||||
if not pcall(music.source.queue, music.source, sd) then return end
|
||||
free = free - 1
|
||||
limit = limit - 1
|
||||
end
|
||||
@@ -174,7 +179,7 @@ local function playMusicSync(data, header, allowLoops)
|
||||
currentMusic = { source = source, engine = engine, threaded = false,
|
||||
started = true, finished = false }
|
||||
fillSync(MUSIC_FILL_INITIAL)
|
||||
if not musicHeld then source:play() end
|
||||
if not musicHeld then pcall(source.play, source) end
|
||||
return source
|
||||
end
|
||||
|
||||
@@ -183,6 +188,9 @@ end
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local musicGen = 0
|
||||
-- bumps when SOUND flips so already-queued PCM (old pan) is dropped rather
|
||||
-- than playing out the ~6s stall-tolerance queue (#1471)
|
||||
local stereoEpoch = 0
|
||||
|
||||
function ChipAudio.playMusic(data, header, allowLoops)
|
||||
if not ensureWorker() then
|
||||
@@ -204,9 +212,11 @@ function ChipAudio.playMusic(data, header, allowLoops)
|
||||
allowLoops = allowLoops, audio = slimAudio(data),
|
||||
channelVolumes = ChipSynth.getChannelVolumes(),
|
||||
channelPitches = ChipSynth.getChannelPitches(),
|
||||
stereo = ChipSynth.getStereo() })
|
||||
stereo = ChipSynth.getStereo(),
|
||||
stereoEpoch = stereoEpoch })
|
||||
currentMusic = { source = source, gen = gen, threaded = true,
|
||||
started = false, finished = false }
|
||||
started = false, finished = false,
|
||||
stereoEpoch = stereoEpoch }
|
||||
-- playback starts in update() once the first buffer arrives (~1 frame)
|
||||
return source
|
||||
end
|
||||
@@ -230,12 +240,16 @@ local function updateThreaded()
|
||||
return
|
||||
end
|
||||
while true do
|
||||
local free = m.source:getFreeBufferCount()
|
||||
local okFree, free = pcall(m.source.getFreeBufferCount, m.source)
|
||||
if not okFree or type(free) ~= "number" then return end
|
||||
local buf = pendingBuf
|
||||
if buf then pendingBuf = nil else buf = outCh:pop() end
|
||||
if not buf then break end
|
||||
if buf.gen ~= m.gen then
|
||||
-- stale buffer from a superseded song: drop it
|
||||
elseif buf.stereoEpoch ~= nil and m.stereoEpoch ~= nil
|
||||
and buf.stereoEpoch ~= m.stereoEpoch then
|
||||
-- stale pan mix from before a live SOUND toggle (#1471)
|
||||
elseif buf.done then
|
||||
m.finished = true
|
||||
elseif buf.error then
|
||||
@@ -243,7 +257,7 @@ local function updateThreaded()
|
||||
m.finished = true
|
||||
elseif buf.sd then
|
||||
if free > 0 then
|
||||
m.source:queue(buf.sd)
|
||||
if not pcall(m.source.queue, m.source, buf.sd) then return end
|
||||
else
|
||||
pendingBuf = buf -- Source full; hold this one for next frame
|
||||
break
|
||||
@@ -251,7 +265,9 @@ local function updateThreaded()
|
||||
end
|
||||
end
|
||||
if not m.started and not musicHeld then
|
||||
if (MUSIC_BUFFER_COUNT - m.source:getFreeBufferCount()) > 0 then
|
||||
local okFree, free = pcall(m.source.getFreeBufferCount, m.source)
|
||||
if okFree and type(free) == "number"
|
||||
and (MUSIC_BUFFER_COUNT - free) > 0 then
|
||||
pcall(function() m.source:play() end)
|
||||
m.started = true
|
||||
end
|
||||
@@ -259,6 +275,7 @@ local function updateThreaded()
|
||||
end
|
||||
|
||||
function ChipAudio.update()
|
||||
if suspended then return end
|
||||
local m = currentMusic
|
||||
if not m then return end
|
||||
if m.threaded then
|
||||
@@ -272,13 +289,16 @@ end
|
||||
-- Music has handled intentional fanfare pauses, so it never fights the normal
|
||||
-- pause/resume behavior.
|
||||
function ChipAudio.ensureMusicPlaying()
|
||||
if suspended then return end
|
||||
local m = currentMusic
|
||||
if not m or m.finished or musicHeld then return end
|
||||
if m.threaded then
|
||||
if not m.started then return end
|
||||
local ok, playing = pcall(function() return m.source:isPlaying() end)
|
||||
if ok and not playing
|
||||
and (MUSIC_BUFFER_COUNT - m.source:getFreeBufferCount()) > 0 then
|
||||
if not ok or playing then return end
|
||||
local okFree, free = pcall(m.source.getFreeBufferCount, m.source)
|
||||
if okFree and type(free) == "number"
|
||||
and (MUSIC_BUFFER_COUNT - free) > 0 then
|
||||
pcall(function() m.source:play() end)
|
||||
end
|
||||
else
|
||||
@@ -354,9 +374,73 @@ function ChipAudio.shutdown()
|
||||
workerReady = false
|
||||
end
|
||||
|
||||
function ChipAudio.currentSource()
|
||||
return currentMusic and currentMusic.source
|
||||
end
|
||||
|
||||
function ChipAudio.setSuspended(flag)
|
||||
suspended = not not flag
|
||||
end
|
||||
|
||||
function ChipAudio.isSuspended()
|
||||
return suspended
|
||||
end
|
||||
|
||||
function ChipAudio.rebuildPlayback()
|
||||
local m = currentMusic
|
||||
if not m then return true end
|
||||
if not (love.audio and love.audio.newQueueableSource) then return false end
|
||||
local ok, source = pcall(
|
||||
love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT)
|
||||
if not ok or not source then return false end
|
||||
pendingBuf = nil
|
||||
local old = m.source
|
||||
m.source = source
|
||||
m.started = false
|
||||
if old then pcall(old.stop, old) end
|
||||
if not m.threaded then
|
||||
fillSync(MUSIC_FILL_INITIAL)
|
||||
if not musicHeld then pcall(source.play, source) end
|
||||
m.started = true
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ChipAudio.setStereo(enabled)
|
||||
enabled = not not enabled
|
||||
if ChipSynth.getStereo() == enabled then return end
|
||||
ChipSynth.setStereo(enabled)
|
||||
pushChannelMix()
|
||||
stereoEpoch = stereoEpoch + 1
|
||||
local m = currentMusic
|
||||
if m and m.engine then
|
||||
ChipSynth.applyStereo(m.engine)
|
||||
end
|
||||
if workerReady and cmdCh then
|
||||
cmdCh:push({ cmd = "channelMix",
|
||||
volumes = ChipSynth.getChannelVolumes(),
|
||||
pitches = ChipSynth.getChannelPitches(),
|
||||
stereo = enabled,
|
||||
stereoEpoch = stereoEpoch })
|
||||
end
|
||||
if not m then return end
|
||||
pendingBuf = nil
|
||||
if outCh then outCh:clear() end
|
||||
m.stereoEpoch = stereoEpoch
|
||||
-- QueueableSource cannot unqueue; swap so the ~6s stall-tolerance buffers
|
||||
-- (mixed under the previous pan) do not have to play out first (#1471)
|
||||
if not love.audio then return end
|
||||
local ok, source = pcall(
|
||||
love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT)
|
||||
if not ok or not source then return end
|
||||
local old = m.source
|
||||
m.source = source
|
||||
m.started = false
|
||||
if old then pcall(old.stop, old) end
|
||||
if not m.threaded then
|
||||
fillSync(MUSIC_FILL_INITIAL)
|
||||
if not musicHeld then pcall(source.play, source) end
|
||||
m.started = true
|
||||
end
|
||||
end
|
||||
|
||||
function ChipAudio.getStereo()
|
||||
|
||||
+49
-4
@@ -294,6 +294,10 @@ function Channel.new(engine, spec, options)
|
||||
noiseSampling = false, -- Gen 2 toggle_noise
|
||||
condition = 0, -- Gen 2 set_condition / sound_jump_if
|
||||
tracks = tracks, -- Gen 2 CHANNEL_TRACKS (NR51 bits for this channel)
|
||||
-- last Music_StereoPanning byte; remembered even while MONO so a live
|
||||
-- SOUND toggle can re-apply it without restarting the song (#1471)
|
||||
stereoPanning = nil,
|
||||
forcePanning = false, -- ForceStereoPanning ($e4) ignores the SOUND option
|
||||
waveInstrument = 0,
|
||||
waveLevel = 1,
|
||||
perfectPitch = false,
|
||||
@@ -406,6 +410,20 @@ function Channel:pan()
|
||||
bit.band(self.engine.pan, mask) ~= 0
|
||||
end
|
||||
|
||||
-- Recompute CHANNEL_TRACKS from the remembered panning byte and the live
|
||||
-- STEREO flag. ForceStereoPanning stays put either way.
|
||||
function Channel:applyStereoMix()
|
||||
local mask = bit.lshift(1, self.hardware - 1)
|
||||
local default = bit.bor(bit.lshift(mask, 4), mask)
|
||||
if self.forcePanning then
|
||||
return
|
||||
elseif stereoEnabled and self.stereoPanning then
|
||||
self.tracks = bit.band(self.stereoPanning, default)
|
||||
else
|
||||
self.tracks = default
|
||||
end
|
||||
end
|
||||
|
||||
function Channel:tone(ticks, register, volume, fade)
|
||||
if register >= 0x800 then
|
||||
return self:timedEvent({ silence = true }, ticks)
|
||||
@@ -756,6 +774,7 @@ function Channel:nextEventGen2()
|
||||
local mask = bit.lshift(1, self.hardware - 1)
|
||||
local default = bit.bor(bit.lshift(mask, 4), mask)
|
||||
self.tracks = bit.band(packed, default)
|
||||
self.forcePanning = true
|
||||
elseif command == 0xE5 then -- volume (global master; ignored for mix)
|
||||
self:byte()
|
||||
elseif command == 0xE6 then -- pitch_offset (big-endian)
|
||||
@@ -778,8 +797,12 @@ function Channel:nextEventGen2()
|
||||
elseif command == 0xEE then -- unknownmusic0xee
|
||||
self:word()
|
||||
elseif command == 0xEF then
|
||||
-- audio/engine.asm:1987 Music_StereoPanning: apply only when STEREO is on
|
||||
-- audio/engine.asm:1987 Music_StereoPanning: apply only when STEREO is on.
|
||||
-- The packed byte is kept either way so ChipSynth.applyStereo can honour
|
||||
-- a live SOUND toggle mid-song (#1471).
|
||||
local packed = self:byte()
|
||||
self.stereoPanning = packed
|
||||
self.forcePanning = false
|
||||
if stereoEnabled then
|
||||
local mask = bit.lshift(1, self.hardware - 1)
|
||||
local default = bit.bor(bit.lshift(mask, 4), mask)
|
||||
@@ -1298,14 +1321,30 @@ function Engine:sampleStereo()
|
||||
local left, right = 0, 0
|
||||
for _, channel in ipairs(self.channels) do
|
||||
local value = channel:sample()
|
||||
local event = channel.event
|
||||
if not event or event.panLeft ~= false then left = left + value end
|
||||
if not event or event.panRight ~= false then right = right + value end
|
||||
local panLeft, panRight
|
||||
if self.generation == 2 then
|
||||
-- live CHANNEL_TRACKS, not the pan baked into the current note, so a
|
||||
-- SOUND toggle reaches the next synthesized sample (#1471)
|
||||
panLeft, panRight = channel:pan()
|
||||
else
|
||||
local event = channel.event
|
||||
panLeft = not event or event.panLeft ~= false
|
||||
panRight = not event or event.panRight ~= false
|
||||
end
|
||||
if panLeft then left = left + value end
|
||||
if panRight then right = right + value end
|
||||
end
|
||||
return analogOut(self, left, "hpfCapLeft", "lpfLeft"),
|
||||
analogOut(self, right, "hpfCapRight", "lpfRight")
|
||||
end
|
||||
|
||||
function Engine:applyStereo()
|
||||
if self.generation ~= 2 then return end
|
||||
for _, channel in ipairs(self.channels) do
|
||||
channel:applyStereoMix()
|
||||
end
|
||||
end
|
||||
|
||||
function Engine:sampleChannel(number)
|
||||
local selected = 0
|
||||
for _, channel in ipairs(self.channels) do
|
||||
@@ -1360,4 +1399,10 @@ ChipSynth.newEngine = Engine.new
|
||||
ChipSynth.soundData = soundData
|
||||
ChipSynth.renderEffectData = renderEffectData
|
||||
|
||||
function ChipSynth.applyStereo(engine)
|
||||
if type(engine) == "table" and engine.applyStereo then
|
||||
engine:applyStereo()
|
||||
end
|
||||
end
|
||||
|
||||
return ChipSynth
|
||||
|
||||
+41
-1
@@ -65,6 +65,8 @@ state = {
|
||||
fade = nil, -- active volume-ramp fade-out (see Music.fadeOut)
|
||||
tempo = nil, -- alternate-tempo override in force for `current`
|
||||
start = nil,
|
||||
data = nil,
|
||||
loop = nil,
|
||||
failed = {}, -- labels whose def could not be started; logged once
|
||||
}
|
||||
|
||||
@@ -319,6 +321,8 @@ function Music.play(data, song, loop, ctx)
|
||||
state.current = song
|
||||
state.tempo = tempo
|
||||
state.start = start
|
||||
state.data = data
|
||||
state.loop = loop
|
||||
if Runtime.wants("music.started") then
|
||||
Runtime.emit("music.started", {
|
||||
song = song, previous = previous, chip = isChip,
|
||||
@@ -334,6 +338,7 @@ function Music.stop()
|
||||
require("src.core.ChipAudio").stopMusic()
|
||||
state.current, state.source, state.loopSource, state.fade = nil, nil, nil, nil
|
||||
state.tempo, state.start = nil, nil
|
||||
state.data, state.loop = nil, nil
|
||||
state.chip = false
|
||||
state.pendingRestore = nil
|
||||
if previous and Runtime.wants("music.stopped") then
|
||||
@@ -501,13 +506,48 @@ function Music.setFilterLevel(level)
|
||||
applyFilter(state.loopSource)
|
||||
end
|
||||
|
||||
function Music.setPitch(pitch)
|
||||
pitch = pitch or 1.0
|
||||
if state.source then pcall(state.source.setPitch, state.source, pitch) end
|
||||
if state.loopSource then pcall(state.loopSource.setPitch, state.loopSource, pitch) end
|
||||
end
|
||||
|
||||
-- re-apply persisted audio options (Game calls this on boot and after
|
||||
-- loading a save)
|
||||
function Music.applyOptions(opts)
|
||||
Music.setVolumeLevel(opts and opts.musicVol or 7)
|
||||
Music.setFilterLevel(opts and opts.musicFilter or 0)
|
||||
-- engine/menus/options_menu.asm SOUND row (wOptions STEREO bit)
|
||||
require("src.core.ChipAudio").setStereo(opts and opts.sound == "STEREO")
|
||||
local ChipAudio = require("src.core.ChipAudio")
|
||||
ChipAudio.setStereo(opts and opts.sound == "STEREO")
|
||||
-- setStereo may swap the queueable source so the new pan is not sitting
|
||||
-- behind already-mixed buffers; re-bind so volume/filter follow (#1471)
|
||||
if state.chip then
|
||||
local src = ChipAudio.currentSource()
|
||||
if src then
|
||||
state.source = src
|
||||
applyVolume(src)
|
||||
applyFilter(src)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Music.onDeviceReset()
|
||||
if not love.audio then return end
|
||||
if state.chip then
|
||||
local src = require("src.core.ChipAudio").currentSource()
|
||||
if not src then return end
|
||||
state.source = src
|
||||
applyVolume(src)
|
||||
applyFilter(src)
|
||||
return
|
||||
end
|
||||
local data, song = state.data, state.current
|
||||
if not (data and song) then return end
|
||||
local loop, tempo, start = state.loop, state.tempo, state.start
|
||||
state.current = nil
|
||||
Music.play(data, song, loop, { reason = "devicereset", selected = true,
|
||||
tempo = tempo, start = start })
|
||||
end
|
||||
|
||||
local function sourceStopped(src)
|
||||
|
||||
+16
-12
@@ -165,12 +165,12 @@ local function playPath(data, key, def, pitch, tempo, plain)
|
||||
reportBadDef("sfx", key, owner(data, "sfx", key), err)
|
||||
return nil
|
||||
end
|
||||
s:setVolume(volumeFor(key))
|
||||
pcall(s.setVolume, s, volumeFor(key))
|
||||
cache[key] = s
|
||||
src = s
|
||||
end
|
||||
src:stop()
|
||||
src:play()
|
||||
pcall(src.stop, src)
|
||||
pcall(src.play, src)
|
||||
return src
|
||||
end
|
||||
|
||||
@@ -619,12 +619,12 @@ function Sound.playPikaCry(data, n)
|
||||
-- extractPikachuCries); widenMono re-decodes to 16-bit stereo so they
|
||||
-- stay off surround outputs (#626). Fresh extracts are already stereo.
|
||||
s = widenMono(s, path)
|
||||
s:setVolume(volumeFor(key))
|
||||
pcall(s.setVolume, s, volumeFor(key))
|
||||
cache[key] = s
|
||||
src = s
|
||||
end
|
||||
src:stop()
|
||||
src:play()
|
||||
pcall(src.stop, src)
|
||||
pcall(src.play, src)
|
||||
played("cry", "PIKACHU_PCM_" .. n, "PIKACHU")
|
||||
return src
|
||||
end
|
||||
@@ -659,12 +659,12 @@ function Sound.playCry(data, species, pikaClip)
|
||||
owner(data, "cries", species), err)
|
||||
return nil
|
||||
end
|
||||
s:setVolume(volumeFor(key))
|
||||
pcall(s.setVolume, s, volumeFor(key))
|
||||
cache[key] = s
|
||||
src = s
|
||||
end
|
||||
src:stop()
|
||||
src:play()
|
||||
pcall(src.stop, src)
|
||||
pcall(src.play, src)
|
||||
played("cry", species, species)
|
||||
return src
|
||||
end
|
||||
@@ -734,12 +734,12 @@ function Sound.startLoop(data, name)
|
||||
reportBadDef("sfx", name, owner(data, "sfx", name), err or "no source")
|
||||
return
|
||||
end
|
||||
s:setLooping(true)
|
||||
s:setVolume(volumeFor(name))
|
||||
pcall(s.setLooping, s, true)
|
||||
pcall(s.setVolume, s, volumeFor(name))
|
||||
loopCache[name] = s
|
||||
src = s
|
||||
end
|
||||
src:play()
|
||||
pcall(src.play, src)
|
||||
looping[name] = src
|
||||
end
|
||||
|
||||
@@ -817,6 +817,10 @@ end
|
||||
-- def is re-resolved on the next play (20 §2 cache contract, audio row)
|
||||
Assets.register(Sound.invalidate)
|
||||
|
||||
function Sound.onDeviceReset()
|
||||
Sound.invalidate()
|
||||
end
|
||||
|
||||
-- re-apply persisted audio options (Game calls this on boot and after
|
||||
-- loading a save)
|
||||
function Sound.applyOptions(opts)
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
-- Protocol -- main thread pushes command tables onto the "chipaudio_cmd"
|
||||
-- channel and drains produced buffers off "chipaudio_out":
|
||||
-- cmd = "play" { gen, header, allowLoops, audio,
|
||||
-- channelVolumes?, channelPitches? }
|
||||
-- channelVolumes?, channelPitches?, stereo?, stereoEpoch? }
|
||||
-- cmd = "stop" halt production
|
||||
-- cmd = "channelMix" { volumes, pitches } per-hw volume/pitch
|
||||
-- cmd = "channelMix" { volumes, pitches, stereo?, stereoEpoch? }
|
||||
-- stereoEpoch present: live SOUND toggle; drop lookahead
|
||||
-- cmd = "invalidate" drop the bank cache
|
||||
-- cmd = "quit" end the thread
|
||||
-- out buffers are tagged with the play's `gen` so the main thread can
|
||||
@@ -39,6 +40,7 @@ local gen = nil -- active song generation, or nil when stopped
|
||||
local engine = nil -- the ChipSynth engine producing the current song
|
||||
local finished = false -- the current song ran out (non-looping)
|
||||
local data = nil -- { audio = <slim audio tables> } for ROM bank/wave reads
|
||||
local stereoEpoch = 0 -- matches ChipAudio; stale pan buffers are dropped
|
||||
|
||||
local function handle(cmd)
|
||||
if cmd.cmd == "play" then
|
||||
@@ -56,6 +58,7 @@ local function handle(cmd)
|
||||
if cmd.stereo ~= nil then
|
||||
ChipSynth.setStereo(cmd.stereo)
|
||||
end
|
||||
if cmd.stereoEpoch ~= nil then stereoEpoch = cmd.stereoEpoch end
|
||||
local ok, eng = pcall(ChipSynth.newEngine, data, cmd.header,
|
||||
{ allowLoops = cmd.allowLoops })
|
||||
if ok then
|
||||
@@ -73,6 +76,11 @@ local function handle(cmd)
|
||||
if cmd.volumes ~= nil then ChipSynth.setChannelVolumes(cmd.volumes) end
|
||||
if cmd.pitches ~= nil then ChipSynth.setChannelPitches(cmd.pitches) end
|
||||
if cmd.stereo ~= nil then ChipSynth.setStereo(cmd.stereo) end
|
||||
if engine and cmd.stereo ~= nil then ChipSynth.applyStereo(engine) end
|
||||
if cmd.stereoEpoch ~= nil then
|
||||
stereoEpoch = cmd.stereoEpoch
|
||||
outCh:clear()
|
||||
end
|
||||
elseif cmd.cmd == "invalidate" then
|
||||
ChipSynth.invalidateBanks()
|
||||
elseif cmd.cmd == "quit" then
|
||||
@@ -95,12 +103,13 @@ while true do
|
||||
local activeGen = gen
|
||||
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
|
||||
if not ok then
|
||||
outCh:push({ gen = activeGen, error = tostring(sd) })
|
||||
outCh:push({ gen = activeGen, error = tostring(sd),
|
||||
stereoEpoch = stereoEpoch })
|
||||
finished = true
|
||||
else
|
||||
outCh:push({ gen = activeGen, sd = sd })
|
||||
outCh:push({ gen = activeGen, sd = sd, stereoEpoch = stereoEpoch })
|
||||
if engine:finished() then
|
||||
outCh:push({ gen = activeGen, done = true })
|
||||
outCh:push({ gen = activeGen, done = true, stereoEpoch = stereoEpoch })
|
||||
finished = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1927,7 +1927,9 @@ function RomExtractorGen2:extractTitle()
|
||||
trail = "assets/generated/title/trail.png",
|
||||
copyright = "assets/generated/title/copyright.png",
|
||||
copyrightSplash = "assets/generated/title/copyright_splash.png",
|
||||
-- ScrollTitleScreenClouds: 1px left every 8 frames (Gold).
|
||||
-- ScrollTitleScreenClouds: Gold decrements the cloud-band SCX every
|
||||
-- 8 vblanks, so the strip slides 1px right. Silver does the same
|
||||
-- decrement every frame.
|
||||
cloudScrollEvery = 8,
|
||||
cloudY = 88,
|
||||
}
|
||||
@@ -4102,8 +4104,30 @@ function RomExtractorGen2:extractEncounters()
|
||||
|
||||
-- FishGroups rows: chance byte then old/good/super rod pointers, each a
|
||||
-- list of (cumulative chance, species, level) triples ending at 100%.
|
||||
-- Rows with species == 0 (time_group in pokegold data/wild/fish.asm) index
|
||||
-- TimeFishGroups [day_species, day_level, nite_species, nite_level].
|
||||
self:trace("fish groups")
|
||||
local fish = self:symbol("FishGroups")
|
||||
local timeFishSym = self.symbols.TimeFishGroups and self:symbol("TimeFishGroups")
|
||||
local timeFishBank = timeFishSym and timeFishSym.bank or (fish and fish.bank)
|
||||
local timeFishAddr = timeFishSym and timeFishSym.address or (fish and 0x6BDE)
|
||||
local timeFishGroups = {}
|
||||
if timeFishBank and timeFishAddr then
|
||||
for idx = 0, 31 do
|
||||
local base = timeFishAddr + idx * 4
|
||||
if not romAddrOk(timeFishBank, base + 3) then break end
|
||||
local daySp = self.rom:byte(timeFishBank, base)
|
||||
local dayLv = self.rom:byte(timeFishBank, base + 1)
|
||||
local niteSp = self.rom:byte(timeFishBank, base + 2)
|
||||
local niteLv = self.rom:byte(timeFishBank, base + 3)
|
||||
if daySp == 0 or daySp > 251 or niteSp == 0 or niteSp > 251 then break end
|
||||
timeFishGroups[idx] = {
|
||||
day = { species = self:speciesName(daySp), level = dayLv },
|
||||
nite = { species = self:speciesName(niteSp), level = niteLv },
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local fishGroups = {}
|
||||
local function readRod(address)
|
||||
local list = {}
|
||||
@@ -4115,11 +4139,24 @@ function RomExtractorGen2:extractEncounters()
|
||||
local chance = self.rom:byte(fish.bank, address + i * 3)
|
||||
local species = self.rom:byte(fish.bank, address + i * 3 + 1)
|
||||
local level = self.rom:byte(fish.bank, address + i * 3 + 2)
|
||||
list[#list + 1] = {
|
||||
chance = chance,
|
||||
species = self:speciesName(species),
|
||||
level = level,
|
||||
}
|
||||
local entry = { chance = chance }
|
||||
if species == 0 then
|
||||
entry.timeGroup = level
|
||||
local tg = timeFishGroups[level]
|
||||
if tg then
|
||||
entry.day = tg.day
|
||||
entry.nite = tg.nite
|
||||
entry.species = tg.day.species
|
||||
entry.level = tg.day.level
|
||||
else
|
||||
entry.species = 0
|
||||
entry.level = level
|
||||
end
|
||||
else
|
||||
entry.species = self:speciesName(species)
|
||||
entry.level = level
|
||||
end
|
||||
list[#list + 1] = entry
|
||||
-- Rows are cumulative and the last one is 100% ($ff after `percent`).
|
||||
if chance >= 0xfe then break end
|
||||
end
|
||||
@@ -4217,6 +4254,7 @@ function RomExtractorGen2:extractEncounters()
|
||||
grass = grass,
|
||||
water = water,
|
||||
fishGroups = fishGroups,
|
||||
timeFishGroups = timeFishGroups,
|
||||
trees = trees,
|
||||
rocks = rocks,
|
||||
treeSets = treeSets,
|
||||
@@ -5020,6 +5058,10 @@ function RomExtractorGen2:extractMenuGfx()
|
||||
question = "QuestionEmote",
|
||||
happy = "HappyEmote",
|
||||
sad = "SadEmote",
|
||||
heart = "HeartEmote",
|
||||
bolt = "BoltEmote",
|
||||
sleep = "SleepEmote",
|
||||
fish = "FishEmote",
|
||||
}) do
|
||||
local symbol = self.symbols[label]
|
||||
if symbol then
|
||||
|
||||
@@ -987,9 +987,14 @@ R.tilesets = {
|
||||
-- for "no fish here, roll the map's water table instead" (pokegold
|
||||
-- data/wild/fish.asm), which is why species is a union rather than a bare id.
|
||||
local gen2Slot = f.rec{ level = f.int(1), species = f.id("pokemon") }
|
||||
-- the sentinel row carries level 0 as well as species 0, so both floors drop
|
||||
-- the sentinel row carries level 0 as well as species 0, so both floors drop.
|
||||
-- Time-dependent rows (TimeFishGroups) carry day/nite sub-slots.
|
||||
local gen2FishSubSlot = f.rec{ level = f.int(0), species = f.union{ f.id("pokemon"), f.int(0, 0) } }
|
||||
local gen2FishSlot = f.rec{ chance = f.int(0, 255), level = f.int(0),
|
||||
species = f.union{ f.id("pokemon"), f.int(0, 0) } }
|
||||
species = f.union{ f.id("pokemon"), f.int(0, 0) },
|
||||
timeGroup = f.opt(f.int(0, 255)),
|
||||
day = f.opt(gen2FishSubSlot),
|
||||
nite = f.opt(gen2FishSubSlot) }
|
||||
-- Headbutt/Rock Smash slots. species is optional and the level floor is 0
|
||||
-- because TreeMonSet_Rock has no `rare` half in the ROM (pokegold
|
||||
-- data/wild/treemons.asm ends the table after the common rows), so the four
|
||||
@@ -1046,6 +1051,8 @@ R.encounters = {
|
||||
chance = f.int(0, 255),
|
||||
old = f.list(gen2FishSlot), good = f.list(gen2FishSlot),
|
||||
super = f.list(gen2FishSlot) }),
|
||||
timeFishGroups = f.opt(f.map(f.union{ f.str, f.int(0, 255) },
|
||||
f.rec{ day = gen2FishSubSlot, nite = gen2FishSubSlot })),
|
||||
-- headbutt: map -> tree set id, and the set's common/rare tables. rocks
|
||||
-- is the same indirection for Rock Smash.
|
||||
trees = f.map(f.str, f.str),
|
||||
|
||||
@@ -305,6 +305,13 @@ function PaletteFX.usesGbcPack(mode)
|
||||
return mode == "redpp"
|
||||
end
|
||||
|
||||
function PaletteFX.honorsTrueColor()
|
||||
if GameVersion.generation() >= 2 then
|
||||
return require("src.render.GbcPalette").mode == "gbc"
|
||||
end
|
||||
return PaletteFX.mode == "redpp"
|
||||
end
|
||||
|
||||
-- Yellow's authentic GBC look is CGBBasePalettes (per-map), not a boot-ROM
|
||||
-- auto-palette. The shared `ogred` save id wears that table on a Yellow
|
||||
-- playthrough and labels itself "OG YELLOW".
|
||||
|
||||
@@ -641,6 +641,7 @@ end
|
||||
-- or empty zone list is left alone: that already draws the whole canvas
|
||||
-- unshaded, which is what the rects were asking for.
|
||||
local function withTrueColor(zoneList, pass)
|
||||
if not PaletteFX.honorsTrueColor() then return zoneList end
|
||||
local rects = PaletteFX.trueColorRects(pass)
|
||||
if not (rects[1] and zoneList and zoneList[1]) then return zoneList end
|
||||
local merged = {}
|
||||
|
||||
@@ -234,8 +234,12 @@ function SpriteRenderer:gen2Obp()
|
||||
self.objGroup .. "|" .. tostring(GbcPalette.mode)
|
||||
end
|
||||
|
||||
local function liveTrueColor(def)
|
||||
return def and def.trueColor and PaletteFX.honorsTrueColor()
|
||||
end
|
||||
|
||||
function SpriteRenderer:resolveImage()
|
||||
if self.def.trueColor then return self.image end
|
||||
if liveTrueColor(self.def) then return self.image end
|
||||
if self.objColors then
|
||||
return getObpImage(self.def.image, self:gen2Obp())
|
||||
end
|
||||
@@ -289,7 +293,7 @@ function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip,
|
||||
local redraw = false
|
||||
-- True-color sheets bypass every palette bake; the screen-space exemption
|
||||
-- is recorded below once the final frame/height is known.
|
||||
if self.def.trueColor then
|
||||
if liveTrueColor(self.def) then
|
||||
image = self.image
|
||||
elseif self.objColors then
|
||||
-- Gen 2: the palette came from the caller (setObjPalette). Like RED++
|
||||
@@ -349,7 +353,7 @@ function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip,
|
||||
drawHeight = math.max(1, self.frameHeight - math.min(8, self.frameHeight))
|
||||
end
|
||||
-- Full-color art claims exactly the portion of the frame that was drawn.
|
||||
if self.def.trueColor then
|
||||
if liveTrueColor(self.def) then
|
||||
PaletteFX.markTrueColor(x, y, self.frameWidth, drawHeight)
|
||||
end
|
||||
blitFrame(image, quad, x, y, flip, redraw, self.frameWidth)
|
||||
@@ -360,10 +364,12 @@ end
|
||||
-- overwrites the sheet's own tiles in VRAM in the original, so it has to be
|
||||
-- recolored and OG-RED-redrawn exactly like the sheet rather than blitted as
|
||||
-- raw DMG shades (#384).
|
||||
function SpriteRenderer:drawTile(path, x, y, flip)
|
||||
function SpriteRenderer:drawTile(path, x, y, flip, quad)
|
||||
local image, redraw = getImage(path), false
|
||||
if self.def.trueColor then
|
||||
if liveTrueColor(self.def) then
|
||||
PaletteFX.markTrueColor(x, y, 16, 8)
|
||||
elseif self.objColors then
|
||||
image = getObpImage(path, self:gen2Obp())
|
||||
elseif PaletteFX.usesGbcPack() then
|
||||
local colors, group = PaletteFX.spriteObp(self.def, self.seed)
|
||||
if colors then image = getObpImage(path, colors, group) end
|
||||
@@ -373,10 +379,23 @@ function SpriteRenderer:drawTile(path, x, y, flip)
|
||||
image = getObpImage(path, PaletteFX.dmgObj())
|
||||
end
|
||||
local iw, ih = image:getDimensions()
|
||||
self.tileQuads = self.tileQuads or {}
|
||||
self.tileQuads[path] = self.tileQuads[path]
|
||||
or love.graphics.newQuad(0, 0, iw, ih, iw, ih)
|
||||
blitFrame(image, self.tileQuads[path], x, y, flip, redraw, iw)
|
||||
local q = quad
|
||||
if not q then
|
||||
self.tileQuads = self.tileQuads or {}
|
||||
self.tileQuads[path] = self.tileQuads[path]
|
||||
or love.graphics.newQuad(0, 0, iw, ih, iw, ih)
|
||||
q = self.tileQuads[path]
|
||||
end
|
||||
local qw = iw
|
||||
if q then
|
||||
if q.getViewport then
|
||||
local _, _, w = q:getViewport()
|
||||
qw = w
|
||||
elseif q.w then
|
||||
qw = q.w
|
||||
end
|
||||
end
|
||||
blitFrame(image, q, x, y, flip, redraw, qw)
|
||||
end
|
||||
|
||||
return SpriteRenderer
|
||||
|
||||
@@ -760,6 +760,8 @@ function TileRenderer:markCellBottomRedraw(cx, cy, camX, camY, colors)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills
|
||||
|
||||
function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
||||
|
||||
+824
-292
File diff suppressed because it is too large
Load Diff
@@ -746,7 +746,8 @@ function BattleState:drawPic(mon, back)
|
||||
-- pokemon.sprite's ctx.trueColor, the same flag Gen 1's Sprites.path hands
|
||||
-- back to its own draw site.
|
||||
local function paint()
|
||||
if colors and not trueColor and GbcPalette.available() then
|
||||
if colors and not (trueColor and GbcPalette.mode == "gbc")
|
||||
and GbcPalette.available() then
|
||||
GbcPalette.with(colors, body)
|
||||
else
|
||||
body()
|
||||
|
||||
@@ -355,6 +355,11 @@ function OptionsMenu:cycle(row, delta)
|
||||
if next_ < 1 then next_ = #row.values end
|
||||
if next_ > #row.values then next_ = 1 end
|
||||
self.options[row.key] = row.values[next_]
|
||||
-- MUSIC VOL applies itself as it steps; SOUND has to as well, or the
|
||||
-- pan sits on the current song until the next map change (#1471)
|
||||
if row.key == "sound" then
|
||||
require("src.core.Music").applyOptions(self.options)
|
||||
end
|
||||
end
|
||||
|
||||
function OptionsMenu:leave_()
|
||||
|
||||
@@ -169,7 +169,7 @@ function TitleState:update(_dt)
|
||||
self.frameCounter = self.frameCounter + 1
|
||||
self:advanceHooh()
|
||||
if self.frameCounter % self.cloudScrollEvery == 0 then
|
||||
self.cloudScroll = (self.cloudScroll + 1) % 160
|
||||
self.cloudScroll = (self.cloudScroll - 1) % 160
|
||||
end
|
||||
self:spawnTrail()
|
||||
self:stepTrails()
|
||||
|
||||
+55
-33
@@ -313,9 +313,8 @@ local TROPHY_BOXES = {
|
||||
}
|
||||
|
||||
-- Script_FishCastRod ends on `pause 40`, and Script_GotABite pauses another 40
|
||||
-- over the bobbing rod before the text lands. ShakeHeadbuttTree counts down
|
||||
-- wFrameCounter from 32. All three are frames at 60 Hz, which is the same
|
||||
-- clock World:step runs on.
|
||||
-- over the bobbing rod before the text lands.
|
||||
-- All are frames at 60 Hz, which is the same clock World:step runs on.
|
||||
local FISH_CAST_FRAMES = 40
|
||||
local FISH_BITE_FRAMES = 40
|
||||
local HEADBUTT_SHAKE_FRAMES = 32
|
||||
@@ -346,10 +345,12 @@ local function sameEncounter(enc) return enc end
|
||||
-- (engine/events/fish.asm Fish) byte for byte.
|
||||
local FISH_ROD_KEY = { OLD_ROD = "old", GOOD_ROD = "good", SUPER_ROD = "super" }
|
||||
|
||||
local function fishVanilla(rod, _mapId, candidates)
|
||||
local function fishVanilla(rod, _mapId, candidates, ctx)
|
||||
if not candidates then return nil end
|
||||
return Encounter.fish({ fishGroups = { hooked = candidates } }, "hooked",
|
||||
FISH_ROD_KEY[rod] or rod or "old", nil)
|
||||
local tod = ctx and (ctx.tod or ctx.daytime)
|
||||
return Encounter.fish({ fishGroups = { hooked = candidates },
|
||||
timeFishGroups = ctx and ctx.encounters and ctx.encounters.timeFishGroups },
|
||||
"hooked", FISH_ROD_KEY[rod] or rod or "old", tod, nil)
|
||||
end
|
||||
|
||||
local function speciesByIndex(pokemon, index)
|
||||
@@ -4291,6 +4292,7 @@ function World:rollFishing(rod)
|
||||
return "nibble"
|
||||
end
|
||||
local roll
|
||||
local tod = self.tod or "DAY"
|
||||
if Runtime.wantsHook("encounter.fishing") then
|
||||
-- Gen 1's three arguments, in Gen 1's order: the rod, the map, and the
|
||||
-- candidate list the chain may inspect or replace before the roll. Gold's
|
||||
@@ -4304,10 +4306,10 @@ function World:rollFishing(rod)
|
||||
roll = Runtime.call("encounter.fishing", fishVanilla, rod, map.id,
|
||||
groups and groups[group],
|
||||
{ fishGroup = group, swarm = swarm, encounters = self.encounters,
|
||||
maps = self.maps, data = game.data })
|
||||
maps = self.maps, data = game.data, tod = tod, daytime = tod })
|
||||
else
|
||||
roll = Encounter.fishSlot(self.encounters, map.id, rod, nil, self.maps,
|
||||
swarm)
|
||||
swarm, tod)
|
||||
end
|
||||
if not roll or not roll.species then return "nibble" end
|
||||
local wild = Mon.new(game.data, roll.species, roll.level)
|
||||
@@ -4770,19 +4772,36 @@ function World:runQueuedScript()
|
||||
end
|
||||
|
||||
-- Script_FishCastRod, then Script_NotEvenANibble or Script_GotABite. Held as
|
||||
-- a frame counter rather than a movement byte stream because the three
|
||||
-- commands involved -- fish_cast_rod ($52), fish_got_bite ($51) and show_emote
|
||||
-- ($54) -- are object ACTION changes, not steps, and Movement.decodeByte has
|
||||
-- nothing to say about them.
|
||||
-- an exact frame counter matching 60 Hz engine ticks.
|
||||
function World:beginFishing(outcome, wild)
|
||||
self.fishing = {
|
||||
phase = "cast", timer = FISH_CAST_FRAMES, outcome = outcome, wild = wild,
|
||||
local p = self.player
|
||||
local d = Map.DELTA[p and p.facing or "down"] or Map.DELTA.down
|
||||
local targetCellX = p and (p.cellX + d[1]) or 0
|
||||
local targetCellY = p and (p.cellY + d[2]) or 0
|
||||
local bobber = {
|
||||
cellX = targetCellX,
|
||||
cellY = targetCellY,
|
||||
px = targetCellX * 16,
|
||||
py = targetCellY * 16,
|
||||
}
|
||||
self.fishing = {
|
||||
phase = "cast",
|
||||
timer = FISH_CAST_FRAMES,
|
||||
outcome = outcome,
|
||||
wild = wild,
|
||||
bobber = bobber,
|
||||
facing = p and p.facing or "down",
|
||||
}
|
||||
if self.player then
|
||||
self.player.fishing = true
|
||||
self.player.fishingState = self.fishing
|
||||
end
|
||||
end
|
||||
|
||||
function World:updateFishing()
|
||||
local st = self.fishing
|
||||
if not st then return end
|
||||
if self.player then self.player.fishingState = st end
|
||||
-- A text box owns the frame while it is up; the script only moves on when
|
||||
-- its own callback fires.
|
||||
if self.textbox or self.choicebox then return end
|
||||
@@ -4791,7 +4810,7 @@ function World:updateFishing()
|
||||
-- StepFunction_GotBite (engine/overworld/map_objects.asm:1430) is one byte
|
||||
-- of animation: OBJECT_SPRITE_Y_OFFSET flipped between 0 and 1 once a
|
||||
-- frame for the length of the bite, which is the rod jerking in the
|
||||
-- player's hands. The cast holds still, so only the bite bobs.
|
||||
-- player's hands.
|
||||
if self.player then
|
||||
self.player.spriteYOffset =
|
||||
(st.phase == "bite" and st.timer % 2 == 1) and 1 or 0
|
||||
@@ -4801,34 +4820,33 @@ function World:updateFishing()
|
||||
if self.player then self.player.spriteYOffset = 0 end
|
||||
if st.phase == "cast" then
|
||||
if st.outcome == "battle" then
|
||||
-- Script_GotABite: four fish_got_bite bobs with the EMOTE_SHOCK bubble
|
||||
-- over the player, then `pause 40` before the rod comes back.
|
||||
st.phase = "bite"
|
||||
st.timer = FISH_BITE_FRAMES
|
||||
self:showEmote(EMOTE_SHOCK, 0, FISH_BITE_FRAMES)
|
||||
return
|
||||
end
|
||||
-- Script_NotEvenANibble (queued by $1 .FishNoBite) and
|
||||
-- Script_NotEvenANibble2 (by $4 .FishNoFish) differ only in the
|
||||
-- wFishingResult they record; both write RodNothingText and fall through
|
||||
-- to the same PutTheRodAway.
|
||||
st.phase = "done"
|
||||
self:showText(Strings(TEXT_ROD_NOTHING), function() self.fishing = nil end)
|
||||
self:showText(Strings(TEXT_ROD_NOTHING), function()
|
||||
self.fishing = nil
|
||||
if self.player then
|
||||
self.player.fishing = nil
|
||||
self.player.fishingState = nil
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
if st.phase == "bite" then
|
||||
st.phase = "done"
|
||||
self:showText(Strings(TEXT_ROD_BITE), function()
|
||||
local wild = st.wild
|
||||
-- PutTheRodAway and closetext come before startbattle, and the state has
|
||||
-- to be gone before the battle is pushed or World:busy would still be
|
||||
-- holding the world when it returns.
|
||||
self.fishing = nil
|
||||
-- FishFunction's `.goodtofish` writes BATTLETYPE_FISH into wBattleType
|
||||
-- alongside the species and level it hooked (engine/events/overworld.asm),
|
||||
-- which is the one condition LureBallMultiplier reads for its x3.
|
||||
if self.player then
|
||||
self.player.fishing = nil
|
||||
self.player.fishingState = nil
|
||||
end
|
||||
if wild then self:startBattle({ wild = wild, battleType = "fish" }) end
|
||||
end)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7819,7 +7837,10 @@ function World:drawGrassOver(entity, ox, oy, s)
|
||||
local tilePalettes = tileset.tilePalettes
|
||||
local tilesPerRow = tileset.tilesPerRow or 16
|
||||
local aw, ah = atlas:getDimensions()
|
||||
local rx, ry = entity.px, entity.py + 4
|
||||
-- Only draw the bottom 8px tile row of the cell (ty = py + 8) over the feet,
|
||||
-- matching Gen 1's drawCellBottomRaw. Starting at py + 4 sampled the top
|
||||
-- tile row and drew grass tufts over the face and torso.
|
||||
local rx, ry = entity.px, entity.py + 8
|
||||
self.grassQuad = self.grassQuad or G.newQuad(0, 0, 8, 8, aw, ah)
|
||||
local quad = self.grassQuad
|
||||
G.setColor(1, 1, 1, 1)
|
||||
@@ -9794,13 +9815,14 @@ function World:drawPeople(s, billboard)
|
||||
else
|
||||
entry.npc:draw(ox, oy, s)
|
||||
end
|
||||
-- ShakeGrass rustle only. The cart also ORs OAM_PRIO onto the lower
|
||||
-- 16x8 (drawGrassOver / IN_GRASS) so the BG tuft covers the feet, but
|
||||
-- stacking that plain grass tile on top of the character with the
|
||||
-- rustle reads as a double overlay here -- keep the walk-through anim.
|
||||
-- ShakeGrass rustle only while moving; drawGrassOver when standing/in grass
|
||||
-- so the BG tuft covers the feet.
|
||||
-- Only the current map's own entities: a ghost's cells belong to a
|
||||
-- neighbour's block list.
|
||||
if entry.ox == 0 and entry.oy == 0 then
|
||||
if entity.inGrass and not (entity.grassShake and entity.moving) then
|
||||
self:drawGrassOver(entity, ox, oy, s)
|
||||
end
|
||||
self:drawGrassShake(entity, ox, oy, s)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user