From 31826b4d39b7d6d9ded52d1530683eaa85f3594b Mon Sep 17 00:00:00 2001 From: 1jamie Date: Mon, 21 Sep 2026 19:28:21 -0500 Subject: [PATCH] Fixed GBA OAM priority mapping and overhead layer partitioning for actors --- src/core/game3/field_view.lua | 187 ++++++++------------ tests/game3_elevation_oam_priority_test.lua | 165 +++++++++-------- 2 files changed, 152 insertions(+), 200 deletions(-) diff --git a/src/core/game3/field_view.lua b/src/core/game3/field_view.lua index fa8045bc..c878e16b 100644 --- a/src/core/game3/field_view.lua +++ b/src/core/game3/field_view.lua @@ -19,9 +19,6 @@ FieldView._loggedPal = false FieldView._nativeBatch = nil FieldView._nativeBatches = nil FieldView._nativeOverBatches = nil -FieldView._nativeOverByRow = nil -FieldView._nativeRows = nil -FieldView._nativeCy0 = nil FieldView._nativeBx = nil FieldView._nativeBy = nil FieldView._nativePair = nil @@ -430,23 +427,34 @@ local function pushBillboard(x, y, camX, camY) return true end -local function isPlayerAboveBg2(playerXOff, playerYOff) - if (playerXOff and playerXOff ~= 0) or (playerYOff and playerYOff ~= 0) then - return true +-- pokefirered/src/event_object_movement.c:8368 sElevationToPriority +local ELEVATION_TO_PRIORITY = { + [0] = 2, [1] = 2, [2] = 2, [3] = 2, + [4] = 1, [5] = 2, [6] = 1, [7] = 2, + [8] = 1, [9] = 2, [10] = 1, [11] = 2, + [12] = 1, [13] = 0, [14] = 0, [15] = 2, +} + +local function actorPriority(a) + if a.kind == "player" then + local PlayerMod = package.loaded["src.core.game3.player"] + if PlayerMod and (PlayerMod.jumping or PlayerMod.surfHopping) then + return 1 + end + local WarpMod = package.loaded["src.core.game3.warp"] + if WarpMod and WarpMod.isEscalatorActive and WarpMod.isEscalatorActive() then + return 1 + end + local SpecialAnim = package.loaded["src.core.game3.special_field_anim"] + if SpecialAnim and SpecialAnim.isActive and SpecialAnim.isActive() then + return 1 + end + local elev = a.elevation or (PlayerMod and PlayerMod.elevation) or 3 + return ELEVATION_TO_PRIORITY[elev] or 2 + else + local elev = a.elevation or (a.obj and (a.obj.elevation or (a.obj.def and a.obj.def.elevation))) or 3 + return ELEVATION_TO_PRIORITY[elev] or 2 end - local PlayerMod = package.loaded["src.core.game3.player"] - if PlayerMod and (PlayerMod.jumping or PlayerMod.surfHopping) then - return true - end - local WarpMod = package.loaded["src.core.game3.warp"] - if WarpMod and WarpMod.isEscalatorActive and WarpMod.isEscalatorActive() then - return true - end - local SpecialAnim = package.loaded["src.core.game3.special_field_anim"] - if SpecialAnim and SpecialAnim.isActive and SpecialAnim.isActive() then - return true - end - return false end local function drawSingleActor(game, mapDef, a, camX, camY) @@ -501,6 +509,7 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk kind = "npc", i = eo.localId, obj = eo.def, + elevation = eo.elevation or (eo.def and eo.def.elevation) or 0, x = (eo.px or (eo.cellX * CELL)) + (eo.raiseX or 0), y = (eo.py or (eo.cellY * CELL)) + (eo.raiseY or 0), sortY = sortY, @@ -532,6 +541,7 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk kind = "npc", i = i, obj = npc.def, + elevation = npc.elevation or (npc.def and npc.def.elevation) or 0, x = npc.px or ((npc.cellX or 0) * CELL), y = npc.py or ((npc.cellY or 0) * CELL), sortY = npc.py or ((npc.cellY or 0) * CELL), @@ -547,6 +557,7 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk kind = "npc", i = i, obj = obj, + elevation = obj.elevation or 0, x = (tonumber(obj.x) or 0) * CELL, y = (tonumber(obj.y) or 0) * CELL, sortY = (tonumber(obj.y) or 0) * CELL, @@ -559,15 +570,15 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk end end - local aboveBg2 = isPlayerAboveBg2(playerXOff, playerYOff) local PlayerMod = package.loaded["src.core.game3.player"] - if not aboveBg2 and (not PlayerMod or (PlayerMod.isVisible and PlayerMod.isVisible())) then + if not PlayerMod or (PlayerMod.isVisible and PlayerMod.isVisible()) then local playerSortY = py if PlayerMod and PlayerMod.moving and PlayerMod.targetY and PlayerMod.targetY > (PlayerMod.cellY or 0) then playerSortY = math.max(playerSortY, PlayerMod.targetY * CELL) end actors[#actors + 1] = { kind = "player", + elevation = PlayerMod and PlayerMod.elevation or 3, x = px + (playerXOff or 0), y = py + (playerYOff or 0), sortY = playerSortY, @@ -580,60 +591,27 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk } end - table.sort(actors, function(a, b) + local underActors = {} + local overActors = {} + for _, a in ipairs(actors) do + a.priority = actorPriority(a) + if (a.priority or 2) < 2 then + overActors[#overActors + 1] = a + else + underActors[#underActors + 1] = a + end + end + + local function sortActors(a, b) local ay = a.sortY or a.y local by = b.sortY or b.y if ay == by then return (a.i or 0) < (b.i or 0) end return ay < by - end) - - return actors -end - -local function drawInterleaved(game, mapDef, camX, camY, actors, rows, cy0) - local actorIdx = 1 - local nActors = #actors - local ox = FieldView._nativeOverOx or 0 - local oy = FieldView._nativeOverOy or 0 - local overByRow = FieldView._nativeOverByRow - - if not overByRow or not rows or rows <= 0 then - for _, a in ipairs(actors) do - drawSingleActor(game, mapDef, a, camX, camY) - end - return end + table.sort(underActors, sortActors) + table.sort(overActors, sortActors) - local topWy = (cy0 or 0) * CELL - while actorIdx <= nActors and (actors[actorIdx].sortY or actors[actorIdx].y) < topWy do - drawSingleActor(game, mapDef, actors[actorIdx], camX, camY) - actorIdx = actorIdx + 1 - end - - for r = 0, rows - 1 do - local rowWy = ((cy0 or 0) + r) * CELL - - local rowOver = overByRow[r] - if rowOver and #rowOver > 0 then - love.graphics.setColor(1, 1, 1, 1) - for _, item in ipairs(rowOver) do - if item.image and item.quad then - love.graphics.draw(item.image, item.quad, ox + item.x, oy + item.y) - end - end - end - - local nextRowWy = rowWy + CELL - while actorIdx <= nActors and (actors[actorIdx].sortY or actors[actorIdx].y) < nextRowWy do - drawSingleActor(game, mapDef, actors[actorIdx], camX, camY) - actorIdx = actorIdx + 1 - end - end - - while actorIdx <= nActors do - drawSingleActor(game, mapDef, actors[actorIdx], camX, camY) - actorIdx = actorIdx + 1 - end + return underActors, overActors end --- Collect visible tile draws grouped by palette slot for batched GbcPalette.with. @@ -817,7 +795,7 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH) list = {} cellsByPair[srcPair] = list end - list[#list + 1] = { mid = mid, x = col * CELL, y = row * CELL, row = row } + list[#list + 1] = { mid = mid, x = col * CELL, y = row * CELL } end end end @@ -840,15 +818,6 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH) local oq = NativeTileset.overQuad(ts, slot) if oq then overBatch:add(oq, cell.x, cell.y) - local rList = FieldView._nativeOverByRow[cell.row] - if rList then - rList[#rList + 1] = { - image = ts.overImage, - quad = oq, - x = cell.x, - y = cell.y, - } - end end end end @@ -867,8 +836,6 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH) FieldView._nativeOverOx = ox FieldView._nativeOverOy = oy FieldView._nativeOverPair = pair - FieldView._nativeRows = rows - FieldView._nativeCy0 = cy0 if FieldView._nativeBatches[pair] then love.graphics.draw(FieldView._nativeBatches[pair], ox, oy) end @@ -1215,12 +1182,30 @@ function FieldView.draw(game, canvasW, canvasH, opts) end end - -- Draw Game3 actors and native overhead tiles interleaved by Y-row. + -- Collect Game3 actors partitioned by OAM priority. + local underActors, overActors = nil, nil if not opts.skipActors then - local actors = collectGame3Actors( + underActors, overActors = collectGame3Actors( game, mapDef, camX, camY, px, py, facing, walkPhase, stepFlip, playerYOff, playerXOff) - drawInterleaved( - game, mapDef, camX, camY, actors, FieldView._nativeRows, FieldView._nativeCy0) + end + + -- Draw Game3 actors with normal priority (under BG1 / overhead layer). + if underActors then + for _, a in ipairs(underActors) do + drawSingleActor(game, mapDef, a, camX, camY) + end + end + + -- pret BG1: metatile top layer covers normal OW sprites (roofs, desk counters, trees). + if usedNative and not opts.actorsOnly then + drawNativeOverTiles() + end + + -- Draw Game3 actors with elevated priority (over BG1 / overhead layer, e.g. bridges/cliffs/jumping/escalators). + if overActors then + for _, a in ipairs(overActors) do + drawSingleActor(game, mapDef, a, camX, camY) + end end -- Tall grass over feet (pret subpriority above avatar). @@ -1243,35 +1228,6 @@ function FieldView.draw(game, canvasW, canvasH, opts) end end - -- pret BG2: metatile top layer covers OW sprites if actors were skipped or non-interleaved. - if usedNative and not opts.actorsOnly and (opts.skipActors or not FieldView._nativeOverByRow) then - drawNativeOverTiles() - end - - -- When riding escalator or flagged above BG2, player sprite is in front of BG2 handrail - if not opts.skipActors and isPlayerAboveBg2(playerXOff, playerYOff) then - local PlayerMod = package.loaded["src.core.game3.player"] - if not PlayerMod or (PlayerMod.isVisible and PlayerMod.isVisible()) then - local daytime = daytimeFor(game, mapDef) - local okOw, OwSprites = pcall(require, "src.core.game3.ow_sprites") - local useOw = okOw and OwSprites and OwSprites.ready and OwSprites.ready() - local gid = useOw and OwSprites.playerGraphicsId(game) or nil - local drew = false - if useOw and gid ~= nil then - drew = OwSprites.draw( - gid, px + playerXOff, py + playerYOff, camX, camY, facing, - (walkPhase == 1 or walkPhase == true) and 1 or 0, stepFlip and true or false) - end - if not drew then - local sr = getSpriteRenderer(game, playerSpriteName(game), "player:p", nil, daytime) - if sr then - sr:draw(px + playerXOff, py + playerYOff, camX, camY, facing, - (walkPhase == 1 or walkPhase == true) and 1 or 0, stepFlip and true or false) - end - end - end - end - -- Pokemon Center heal machine (screen-space OAM, pret FLDEFF_POKECENTER_HEAL). if not opts.actorsOnly then local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects") @@ -1301,9 +1257,6 @@ function FieldView.invalidate() FieldView._nativeBatch = nil FieldView._nativeBatches = nil FieldView._nativeOverBatches = nil - FieldView._nativeOverByRow = nil - FieldView._nativeRows = nil - FieldView._nativeCy0 = nil FieldView._nativeBx = nil FieldView._nativeBy = nil FieldView._nativePair = nil diff --git a/tests/game3_elevation_oam_priority_test.lua b/tests/game3_elevation_oam_priority_test.lua index 62eaf996..99657c73 100644 --- a/tests/game3_elevation_oam_priority_test.lua +++ b/tests/game3_elevation_oam_priority_test.lua @@ -93,111 +93,110 @@ do assert_eq("EventObject elevation at (5,6) is updated to 4", eo.elevation, 4) end -print("[test] 3. Interleaved row-by-row overhead and actor draw order") +print("[test] 3. GBA OAM Priority vs Overhead Layer partitioning") do - local drawLog = {} - _G.love = _G.love or {} - _G.love.graphics = _G.love.graphics or {} - love.graphics.draw = function(drawable, ...) - local args = { ... } - if type(drawable) == "string" then - drawLog[#drawLog + 1] = drawable - elseif type(drawable) == "table" and drawable.tag then - drawLog[#drawLog + 1] = drawable.tag + local ELEVATION_TO_PRIORITY = { + [0] = 2, [1] = 2, [2] = 2, [3] = 2, + [4] = 1, [5] = 2, [6] = 1, [7] = 2, + [8] = 1, [9] = 2, [10] = 1, [11] = 2, + [12] = 1, [13] = 0, [14] = 0, [15] = 2, + } + + local actorPriority = function(a) + if a.kind == "player" then + if a.jumping or a.surfHopping or a.escalatorActive or a.specialAnimActive then + return 1 + end + local elev = a.elevation or 3 + return ELEVATION_TO_PRIORITY[elev] or 2 else - drawLog[#drawLog + 1] = "quad" + local elev = a.elevation or (a.obj and a.obj.elevation) or 3 + return ELEVATION_TO_PRIORITY[elev] or 2 end end - love.graphics.setColor = function() end - love.graphics.push = function() end - love.graphics.pop = function() end - love.graphics.translate = function() end - -- Setup overhead rows: - -- Row 5: Counter overhead tile (wy = 5 * 16 = 80) - -- Row 17: Cliff upper edge overhead tile (wy = 17 * 16 = 272) - FieldView._nativeOverByRow = { - [0] = {}, - [1] = {}, - [2] = {}, - [3] = {}, - [4] = {}, - [5] = { { image = { tag = "OVERHEAD_ROW_5_COUNTER" }, quad = {} } }, - [6] = {}, - [16] = {}, - [17] = { { image = { tag = "OVERHEAD_ROW_17_CLIFF" }, quad = {} } }, - [18] = {}, - } - FieldView._nativeOverOx = 0 - FieldView._nativeOverOy = 0 - - -- Setup actors: - -- Actor A: NPC behind counter at row 4 (sortY = 4 * 16 = 64) - -- Actor B: Player in front of counter at row 6 (sortY = 6 * 16 = 96) - -- Actor C: NPC standing behind cliff in water at row 16 (sortY = 16 * 16 = 256) - -- Actor D: Player standing ON cliff upper edge at row 17 (sortY = 17 * 16 = 272) - local actors = { - { tag = "ACTOR_NPC_BEHIND_COUNTER", sortY = 64, x = 0, y = 64 }, - { tag = "ACTOR_PLAYER_IN_FRONT_OF_COUNTER", sortY = 96, x = 0, y = 96 }, - { tag = "ACTOR_NPC_IN_WATER", sortY = 256, x = 0, y = 256 }, - { tag = "ACTOR_PLAYER_ON_CLIFF_EDGE", sortY = 272, x = 0, y = 272 }, - } - - table.sort(actors, function(a, b) return a.sortY < b.sortY end) - - drawLog = {} - - local actorIdx = 1 - local nActors = #actors - local cy0 = 0 - local rows = 20 - local overByRow = FieldView._nativeOverByRow - - for r = 0, rows - 1 do - local rowWy = (cy0 + r) * CELL - - local rowOver = overByRow[r] - if rowOver and #rowOver > 0 then - for _, item in ipairs(rowOver) do - love.graphics.draw(item.image, item.quad, item.x or 0, item.y or 0) + local drawLog = {} + local function simulateDraw(actors) + local underActors = {} + local overActors = {} + for _, a in ipairs(actors) do + a.priority = actorPriority(a) + if a.priority < 2 then + overActors[#overActors + 1] = a + else + underActors[#underActors + 1] = a end end - local nextRowWy = rowWy + CELL - while actorIdx <= nActors and actors[actorIdx].sortY < nextRowWy do - love.graphics.draw(actors[actorIdx].tag) - actorIdx = actorIdx + 1 + local function sortActors(a, b) + local ay = a.sortY or a.y + local by = b.sortY or b.y + if ay == by then return (a.i or 0) < (b.i or 0) end + return ay < by + end + table.sort(underActors, sortActors) + table.sort(overActors, sortActors) + + drawLog = {} + -- 1. Draw underActors (OAM Priority >= 2) + for _, a in ipairs(underActors) do + drawLog[#drawLog + 1] = a.tag + end + -- 2. Draw Overhead layer (BG1, Priority 1) + drawLog[#drawLog + 1] = "OVERHEAD_BG1_LAYER" + -- 3. Draw overActors (OAM Priority < 2, e.g. elevation 4 cliff/bridge, jumping, escalator) + for _, a in ipairs(overActors) do + drawLog[#drawLog + 1] = a.tag end end + -- Scenario A: Nurse Joy behind desk (row 5) and Player on ground (row 6) + -- Both have ground elevation 3 -> Priority 2. Both drawn BEFORE overhead BG1 layer. + -- The overhead desk covers Nurse Joy; the overhead roof covers player legs. + local testActors = { + { tag = "NURSE_JOY_ROW_5", elevation = 3, y = 80, sortY = 80, kind = "npc" }, + { tag = "PLAYER_ON_GROUND_ROW_6", elevation = 3, y = 96, sortY = 96, kind = "player" }, + { tag = "PLAYER_ON_CLIFF_ELEV_4", elevation = 4, y = 272, sortY = 272, kind = "player" }, + { tag = "NPC_IN_WATER_ELEV_1", elevation = 1, y = 256, sortY = 256, kind = "npc" }, + } + + simulateDraw(testActors) + print("Draw order result:") for idx, entry in ipairs(drawLog) do print(string.format(" [%d] %s", idx, entry)) end - assert_eq("Draw step 1 is NPC behind counter", drawLog[1], "ACTOR_NPC_BEHIND_COUNTER") - assert_eq("Draw step 2 is Counter overhead (drawn after NPC behind counter)", drawLog[2], "OVERHEAD_ROW_5_COUNTER") - assert_eq("Draw step 3 is Player in front of counter (drawn after counter)", drawLog[3], "ACTOR_PLAYER_IN_FRONT_OF_COUNTER") - assert_eq("Draw step 4 is NPC in water", drawLog[4], "ACTOR_NPC_IN_WATER") - assert_eq("Draw step 5 is Cliff overhead (drawn after NPC in water, covering them)", drawLog[5], "OVERHEAD_ROW_17_CLIFF") - assert_eq("Draw step 6 is Player on cliff edge (drawn AFTER cliff overhead, on top of edge!)", drawLog[6], "ACTOR_PLAYER_ON_CLIFF_EDGE") + assert_eq("Under-actor 1 is Nurse Joy (behind desk)", drawLog[1], "NURSE_JOY_ROW_5") + assert_eq("Under-actor 2 is Player on ground (behind roof)", drawLog[2], "PLAYER_ON_GROUND_ROW_6") + assert_eq("Under-actor 3 is NPC in water", drawLog[3], "NPC_IN_WATER_ELEV_1") + assert_eq("Step 4 is Overhead BG1 layer (covers Nurse Joy and player on ground)", drawLog[4], "OVERHEAD_BG1_LAYER") + assert_eq("Step 5 is Player on cliff (elevation 4, drawn ON TOP of overhead layer)", drawLog[5], "PLAYER_ON_CLIFF_ELEV_4") end -print("[test] 4. isPlayerAboveBg2 special states") +print("[test] 4. Player priority override states (jumping, escalator, surf hopping)") do - local isPlayerAboveBg2_test = function(playerXOff, playerYOff, isJumping, isEscalator) - if (playerXOff and playerXOff ~= 0) or (playerYOff and playerYOff ~= 0) then - return true + local ELEVATION_TO_PRIORITY = { + [0] = 2, [1] = 2, [2] = 2, [3] = 2, + [4] = 1, [5] = 2, [6] = 1, [7] = 2, + [8] = 1, [9] = 2, [10] = 1, [11] = 2, + [12] = 1, [13] = 0, [14] = 0, [15] = 2, + } + + local computePlayerPriority = function(opts) + if opts.jumping or opts.surfHopping or opts.escalatorActive or opts.specialAnimActive then + return 1 end - if isJumping then return true end - if isEscalator then return true end - return false + local elev = opts.elevation or 3 + return ELEVATION_TO_PRIORITY[elev] or 2 end - assert_true("Jumping player is flagged above BG2", isPlayerAboveBg2_test(0, 0, true, false)) - assert_true("Escalator player is flagged above BG2", isPlayerAboveBg2_test(0, 0, false, true)) - assert_true("Subpixel Y offset player is flagged above BG2", isPlayerAboveBg2_test(0, -8, false, false)) - assert_eq("Normal walking player is not forced above BG2", isPlayerAboveBg2_test(0, 0, false, false), false) + assert_eq("Jumping player has Priority 1 (above BG1)", computePlayerPriority({ jumping = true, elevation = 3 }), 1) + assert_eq("Escalator player has Priority 1 (above BG1)", computePlayerPriority({ escalatorActive = true, elevation = 3 }), 1) + assert_eq("Surf hopping player has Priority 1 (above BG1)", computePlayerPriority({ surfHopping = true, elevation = 1 }), 1) + assert_eq("Bridge player has Priority 1 (above BG1)", computePlayerPriority({ elevation = 4 }), 1) + assert_eq("Normal walking player with animation offsets has Priority 2 (under BG1)", computePlayerPriority({ elevation = 3, spriteYOffset = -8 }), 2) + assert_eq("Water surfing player has Priority 2 (under BG1)", computePlayerPriority({ elevation = 1 }), 2) end print("[test] ALL ELEVATION AND OAM CONDITIONAL PRIORITY TESTS PASSED!")