also fixes feet layering of gold with grass. Unifies gen1/gen2 grass layering into a single system
This commit is contained in:
1jamie
2026-08-18 15:37:27 -05:00
parent def967a8f8
commit 286988a1e3
13 changed files with 1469 additions and 439 deletions
+19 -5
View File
@@ -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
+6
View File
@@ -501,6 +501,12 @@ 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)
+45 -5
View File
@@ -4102,8 +4102,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 +4137,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 +4252,7 @@ function RomExtractorGen2:extractEncounters()
grass = grass,
water = water,
fishGroups = fishGroups,
timeFishGroups = timeFishGroups,
trees = trees,
rocks = rocks,
treeSets = treeSets,
@@ -5020,6 +5056,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
+9 -2
View File
@@ -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),
+20 -5
View File
@@ -360,10 +360,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
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 +375,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
-62
View File
@@ -760,35 +760,7 @@ function TileRenderer:markCellBottomRedraw(cx, cy, camX, camY, colors)
end
end
-- Draw the bottom tile row of every visible grass cell after sprites.
-- DMG/SGB: one SpriteBatch draw under the color-0-key shader.
-- GBC: per-cell draws using pre-keyed images (can't share one batch).
function TileRenderer:drawGrassOverdraw(camX, camY)
local ox, oy = -math.floor(camX), -math.floor(camY)
if self.gbcCtx then
if self.grassCells then
for _, c in ipairs(self.grassCells) do
self:drawCellBottomRaw(c[1], c[2], camX, camY)
end
end
else
local shader = getColor0KeyShader()
if shader then love.graphics.setShader(shader) end
if self.grassBatch then
love.graphics.draw(self.grassBatch, ox, oy)
end
if shader then love.graphics.setShader() end
end
end
-- Queue every visible grass cell's bottom row for the post-zone
-- sprite-redraw pass (GBC OBP replay; mirrors markCellBottomRedraw).
function TileRenderer:markGrassOverdrawRedraw(camX, camY, colors)
if not self.grassCells then return end
for _, c in ipairs(self.grassCells) do
self:markCellBottomRedraw(c[1], c[2], camX, camY, colors)
end
end
local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills
@@ -815,16 +787,6 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
self.winBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
end
self.winBatch:clear()
-- grass-overdraw pass structures: a SpriteBatch for the DMG/SGB shader
-- path (one atlas, one shader draw call) and a plain list for the GBC
-- path (per-cell pre-keyed images can't share a single SpriteBatch).
if not self.gbcCtx then
if not self.grassBatch then
self.grassBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
end
self.grassBatch:clear()
end
self.grassCells = {}
local anims = self.anims
for _, anim in ipairs(anims) do
if not anim.batch then
@@ -834,9 +796,6 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
end
local map, quads = self.map, self.quads
local claimedBy, aliasMap = self.claimedBy, self.aliasMap
-- track which grass cells we've already added so each cx/cy pair is only
-- recorded once even though its two bottom-row tiles share the same cell.
local grassSeen = {}
for ty = ty0, ty1 - 1 do
local by = math.floor(ty / 4)
local ty4 = ty % 4
@@ -859,25 +818,6 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
anim.batch:add(wx, wy)
end
end
-- bottom tile row of a 2×2 cell (ty is odd) that is a grass cell:
-- record it for the post-sprite grass overdraw pass.
if ty % 2 == 1 then
local cx, cy = math.floor(tx / 2), math.floor(ty / 2)
local key = cy * 65536 + cx
if not grassSeen[key] and map:isGrassCell(cx, cy) then
grassSeen[key] = true
self.grassCells[#self.grassCells + 1] = { cx, cy }
-- DMG/SGB path: bake both tiles of the bottom row into the batch
if self.grassBatch then
-- left tile (tx = cx*2)
local lq = quads[map:tileAt(cx * 2, ty)]
if lq then self.grassBatch:add(lq, cx * 16, ty * 8) end
-- right tile (tx = cx*2+1)
local rq = quads[map:tileAt(cx * 2 + 1, ty)]
if rq then self.grassBatch:add(rq, cx * 16 + 8, ty * 8) end
end
end
end
end
end
end
@@ -953,8 +893,6 @@ end
-- :release on eviction.
function TileRenderer:releaseBatches()
safeRelease(self.winBatch); self.winBatch = nil
safeRelease(self.grassBatch); self.grassBatch = nil
self.grassCells = nil
safeRelease(self.borderFill); self.borderFill = nil
safeRelease(self.borderQuad); self.borderQuad = nil
-- shared shift-variant cache; only drop the reference
+824 -292
View File
File diff suppressed because it is too large Load Diff
+33 -34
View File
@@ -5303,16 +5303,25 @@ function OverworldState:drawWorld()
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
and e == self.player) then
e:draw(cam.x, cam.y)
-- tall grass overdraws the sprite's feet (GB sprite priority);
-- the overdraw is BG tiles, so it rides the shake offset too
love.graphics.setColor(1, 1, 1, 1)
if self.map:isGrassCell(e.cellX, e.cellY) then
self.map.renderer:drawCellBottom(e.cellX, e.cellY, cam.x, bgY)
if grassColors then
self.map.renderer:markCellBottomRedraw(e.cellX, e.cellY,
cam.x, bgY, grassColors)
end
end
if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then
self.map.renderer:drawCellBottom(e.targetX, e.targetY, cam.x, bgY)
if grassColors then
self.map.renderer:markCellBottomRedraw(e.targetX, e.targetY,
cam.x, bgY, grassColors)
end
end
end
end
-- tall grass overdraws every visible grass cell's feet row after all
-- sprites (GB sprite-priority parity). One pass regardless of how many
-- entities are on screen -- see TileRenderer:drawGrassOverdraw.
love.graphics.setColor(1, 1, 1, 1)
self.map.renderer:drawGrassOverdraw(cam.x, bgY)
if grassColors then
self.map.renderer:markGrassOverdrawRedraw(cam.x, bgY, grassColors)
end
fxHeal()
fxDust()
fxCutTree()
@@ -5348,18 +5357,6 @@ function OverworldState:drawWorld()
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
end
end
-- Inject grass-cell overdraw items into the same depth-sorted queue so
-- they occlude entities at lower y correctly (back-to-front by cell foot).
-- Each cell's foot y = cy*16 + 16 in world pixels (bottom of its two rows).
local grassCells = self.map.renderer.grassCells
if grassCells then
for _, c in ipairs(grassCells) do
local cx, cy = c[1], c[2]
-- world-pixel foot of the grass cell's bottom tile row
local cellFootY = (cy * 2 + 2) * 8 -- == cy*16+16
items[#items + 1] = { y = cellFootY, kind = "grass", cx = cx, cy = cy }
end
end
table.sort(items, function(a, b) return a.y < b.y end)
for _, it in ipairs(items) do
@@ -5371,20 +5368,6 @@ function OverworldState:drawWorld()
local fy = g.npc.py - cam.y + g.oy + 16
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false,
function() g.npc:draw(cam.x - g.ox, cam.y - g.oy) end)
elseif it.kind == "grass" then
-- tall-grass bottom-row overdraw: billboarded at the cell's foot so
-- it depth-sorts correctly against any entity in the same y column.
-- bgY keeps the elevator-shake offset; drawCellBottomRaw lets the
-- billboard own the shader (color-0 keying baked into the keyed image
-- on GBC, or applied by drawCellBottom's shader on DMG/SGB).
local cx, cy = it.cx, it.cy
local fx = cx * 16 - cam.x + 8 -- horizontal centre of the cell
local fy = (cy * 2 + 2) * 8 - cam.y -- foot of the bottom tile row
local colors = zoneColorsAt(zones, fx, fy)
self:billboard(fx, fy, vw, vh, colors, true, function()
love.graphics.setColor(1, 1, 1, 1)
self.map.renderer:drawCellBottomRaw(cx, cy, cam.x, bgY)
end)
else
local e = it.e
local fx = e.px - cam.x + 8
@@ -5392,6 +5375,22 @@ function OverworldState:drawWorld()
local colors = zoneColorsAt(zones, fx, fy)
self:billboard(fx, fy, vw, vh, colors, false,
function() e:draw(cam.x, cam.y) end)
-- tall-grass feet overdraw glued to the sprite: same anchor + depth
-- so it keeps hiding the feet, color-0-keyed palette so its white
-- gaps still show the sprite through (drawCellBottomRaw lets the
-- billboard own the shader; bgY keeps the elevator-shake offset).
if self.map:isGrassCell(e.cellX, e.cellY) then
self:billboard(fx, fy, vw, vh, colors, true, function()
love.graphics.setColor(1, 1, 1, 1)
self.map.renderer:drawCellBottomRaw(e.cellX, e.cellY, cam.x, bgY)
end)
end
if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then
self:billboard(fx, fy, vw, vh, colors, true, function()
love.graphics.setColor(1, 1, 1, 1)
self.map.renderer:drawCellBottomRaw(e.targetX, e.targetY, cam.x, bgY)
end)
end
end
end
+55 -33
View File
@@ -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