diff --git a/src/render/TileRenderer.lua b/src/render/TileRenderer.lua index f616d252..6f46334e 100644 --- a/src/render/TileRenderer.lua +++ b/src/render/TileRenderer.lua @@ -760,6 +760,36 @@ 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 function TileRenderer:ensureWindow(camX, camY, vw, vh) @@ -785,6 +815,16 @@ 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 @@ -794,6 +834,9 @@ 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 @@ -816,6 +859,25 @@ 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 @@ -891,6 +953,8 @@ 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 diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 0dbc0770..b93fa8bc 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -5303,25 +5303,16 @@ 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() @@ -5357,6 +5348,18 @@ 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 @@ -5368,6 +5371,20 @@ 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 @@ -5375,22 +5392,6 @@ 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