mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-29 23:14:53 -04:00
1331 lines
45 KiB
Lua
1331 lines
45 KiB
Lua
-- Game3 native field renderer (FRLG 240×160, 1:1 world pixels).
|
||
-- Owns tile palette blits + OW sprites. Does NOT call World:draw / Zoom / Chrome.
|
||
|
||
local Display = require("src.core.game3.display")
|
||
local Versions = require("src.import.gba.versions")
|
||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||
|
||
local FieldView = {}
|
||
|
||
FieldView._atlas = nil
|
||
FieldView._atlasPath = nil
|
||
FieldView._quads = {}
|
||
FieldView._tilesPerRow = 16
|
||
FieldView._blockTiles = nil
|
||
FieldView._tilePalettes = nil -- [tileId+1] = BG slot 1..8
|
||
FieldView._spriteCache = {} -- key → SpriteRenderer
|
||
FieldView._logged = false
|
||
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
|
||
FieldView._nativeDirty = true
|
||
FieldView._nativeOverOx = 0
|
||
FieldView._nativeOverOy = 0
|
||
FieldView._nativeOverPair = nil
|
||
FieldView._loggedNative = false
|
||
FieldView._loggedNativeFallback = false
|
||
|
||
-- pokefirered/src/field_screen_effect.c:18
|
||
local FLASH_LEVEL_RADIUS = { [0] = 200, 72, 56, 40, 24 }
|
||
-- pokefirered/include/constants/flags.h:1333
|
||
local FLAG_SYS_FLASH_ACTIVE = 0x806
|
||
|
||
FieldView.MAX_FLASH_LEVEL = 4
|
||
FieldView.flashLevel = 0
|
||
FieldView.cameraPanX = 0
|
||
FieldView.cameraPanY = 0
|
||
FieldView._flashRadius = nil
|
||
FieldView._flashMapId = nil
|
||
FieldView._flashSpans = nil
|
||
FieldView._flashSpanR = nil
|
||
FieldView._flashSpanCX = nil
|
||
FieldView._flashSpanCY = nil
|
||
FieldView._flashSpanW = nil
|
||
FieldView._flashSpanH = nil
|
||
|
||
local CELL = 16
|
||
local BLOCK = 32
|
||
|
||
-- pokefirered/src/event_object_movement.c:4992
|
||
local PLAYER_SCREEN_X = 112
|
||
-- pokefirered/src/field_camera.c:527
|
||
local PLAYER_SCREEN_Y = 72
|
||
|
||
local function screenAnchor(px, py, camX, camY)
|
||
return (px - camX) - PLAYER_SCREEN_X, (py - camY) - PLAYER_SCREEN_Y
|
||
end
|
||
|
||
local function log(msg)
|
||
print("[game3/field] " .. tostring(msg))
|
||
end
|
||
|
||
local function resolveMapDef(game, mapId)
|
||
if not mapId then return nil end
|
||
local data = game and game.data
|
||
return data and data.maps and data.maps[mapId]
|
||
end
|
||
|
||
local function resolveTileset(game, mapDef)
|
||
if not mapDef then return nil end
|
||
local tsId = mapDef.tileset
|
||
if not tsId and mapDef.pair and Versions.PAIR_TILESET then
|
||
tsId = Versions.PAIR_TILESET[mapDef.pair]
|
||
end
|
||
local data = game and game.data
|
||
local sets = data and (data.tilesets or data.gen2Tilesets)
|
||
return tsId and sets and sets[tsId], tsId
|
||
end
|
||
|
||
local function loadAtlas(tileset)
|
||
if not tileset or not tileset.image then return nil end
|
||
local path = tileset.image
|
||
if FieldView._atlas and FieldView._atlasPath == path then
|
||
FieldView._blockTiles = tileset.blocks
|
||
FieldView._tilePalettes = tileset.tilePalettes
|
||
FieldView._tilesPerRow = tileset.tilesPerRow or FieldView._tilesPerRow
|
||
return FieldView._atlas
|
||
end
|
||
local img
|
||
local ok, Assets = pcall(require, "src.render.Assets")
|
||
if ok and Assets and Assets.image then
|
||
local aok, aimg = pcall(Assets.image, path)
|
||
if aok then img = aimg end
|
||
end
|
||
if not img then
|
||
local iok, iimg = pcall(love.graphics.newImage, path)
|
||
if iok then img = iimg end
|
||
end
|
||
if not img then
|
||
log("atlas load failed: " .. tostring(path))
|
||
return nil
|
||
end
|
||
if img.setFilter then img:setFilter("nearest", "nearest") end
|
||
FieldView._atlas = img
|
||
FieldView._atlasPath = path
|
||
FieldView._quads = {}
|
||
FieldView._tilesPerRow = tileset.tilesPerRow or 16
|
||
FieldView._blockTiles = tileset.blocks
|
||
FieldView._tilePalettes = tileset.tilePalettes
|
||
if not FieldView._logged then
|
||
log("native atlas ready path=" .. tostring(path)
|
||
.. " tilesPerRow=" .. tostring(FieldView._tilesPerRow)
|
||
.. " hasTilePalettes=" .. tostring(FieldView._tilePalettes ~= nil))
|
||
FieldView._logged = true
|
||
end
|
||
return img
|
||
end
|
||
|
||
local function quadFor(tile)
|
||
local q = FieldView._quads[tile]
|
||
if q then return q end
|
||
local atlas = FieldView._atlas
|
||
if not atlas then return nil end
|
||
local per = FieldView._tilesPerRow
|
||
local sx = (tile % per) * 8
|
||
local sy = math.floor(tile / per) * 8
|
||
q = love.graphics.newQuad(sx, sy, 8, 8, atlas:getDimensions())
|
||
FieldView._quads[tile] = q
|
||
return q
|
||
end
|
||
|
||
local function blockIdAt(mapDef, bx, by)
|
||
local w = mapDef.width or 0
|
||
local h = mapDef.height or 0
|
||
local blocks = mapDef.blocks
|
||
if not blocks or w < 1 then return mapDef.borderBlock or 0 end
|
||
if bx < 0 or by < 0 or bx >= w or by >= h then
|
||
return mapDef.borderBlock or 0
|
||
end
|
||
return blocks[by * w + bx + 1] or 0
|
||
end
|
||
|
||
--- Resolve Sevii special BG palette set (8 slots × 4 RGB).
|
||
local function resolveBgSet(game, mapDef, daytime)
|
||
local ok, Palettes = pcall(require, "src.world.gen2.Palettes")
|
||
if not ok or not Palettes then return nil end
|
||
local data = game and game.data
|
||
local pals = data and (data.gen2Palettes or data.palettes)
|
||
if not pals then return nil end
|
||
-- Sevii uses specialTilesets[SEVII_*]; prefer specialSet (does not need bg pool).
|
||
if Palettes.specialSet then
|
||
local set = Palettes.specialSet(pals, mapDef)
|
||
if set then return set end
|
||
end
|
||
if Palettes.bgSet then
|
||
return Palettes.bgSet(pals, mapDef, daytime or "DAY")
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function currentMapId(game)
|
||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||
if session and session.map then return session.map end
|
||
local world = game and (game.overworld or game.world)
|
||
if world and world.map and world.map.id then return world.map.id end
|
||
local pos = game and game.save and game.save.position
|
||
return pos and pos.map
|
||
end
|
||
|
||
local function playerPixels(game)
|
||
-- Game3 avatar is source of truth while Runtime is active.
|
||
local okP, G3Player = pcall(require, "src.core.game3.player")
|
||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||
if okP and G3Player and Runtime and Runtime.isActive and Runtime.isActive() then
|
||
local xOff = G3Player.spriteXOffset or 0
|
||
local yOff = G3Player.spriteYOffset or 0
|
||
if yOff == 0 and G3Player.jumpSpriteY then
|
||
yOff = G3Player.jumpSpriteY() or 0
|
||
end
|
||
return G3Player.px, G3Player.py, G3Player.facing or "down",
|
||
G3Player.walkPhase(), G3Player.drawFlip(), G3Player, yOff, xOff
|
||
end
|
||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||
local world = game and (game.overworld or game.world)
|
||
local p = world and world.player
|
||
if p then
|
||
local px = tonumber(p.px)
|
||
local py = tonumber(p.py)
|
||
if not px then px = (tonumber(p.cellX or p.x) or 0) * CELL end
|
||
if not py then py = (tonumber(p.cellY or p.y) or 0) * CELL end
|
||
local facing = p.facing or "down"
|
||
local walkPhase = 0
|
||
if type(p.walkPhase) == "function" then
|
||
walkPhase = p:walkPhase() or 0
|
||
else
|
||
walkPhase = p.movePhase or p.walkPhase or 0
|
||
end
|
||
local stepFlip = false
|
||
if type(p.drawFlip) == "function" then
|
||
stepFlip = p:drawFlip() and true or false
|
||
else
|
||
stepFlip = p.stepFlip and true or false
|
||
end
|
||
return px, py, facing, walkPhase, stepFlip, p, p.spriteYOffset or 0, p.spriteXOffset or 0
|
||
end
|
||
if session then
|
||
local cx, cy = session.x or 0, session.y or 0
|
||
return cx * CELL, cy * CELL, session.facing or "down", 0, false, nil, 0, 0
|
||
end
|
||
return 0, 0, "down", 0, false, nil, 0, 0
|
||
end
|
||
|
||
local function facingFromObj(obj)
|
||
local r = tostring(obj.facing or obj.range or "DOWN"):lower()
|
||
if r == "up" or r == "down" or r == "left" or r == "right" then
|
||
return r
|
||
end
|
||
return "down"
|
||
end
|
||
|
||
local function spriteNameForObj(obj)
|
||
if type(obj.sprite) == "string" and obj.sprite ~= "" then
|
||
return obj.sprite
|
||
end
|
||
local g = obj.graphics or obj.graphicsId
|
||
return GfxIds.spriteFor(g)
|
||
end
|
||
|
||
local function playerSpriteName(game)
|
||
local save = game and game.save
|
||
local session = game and game.session
|
||
local gender = (session and session.gender)
|
||
or (save and (save.gender or (save.player and save.player.gender)))
|
||
if gender == "female" or gender == "F" or gender == 1 then
|
||
return "SPRITE_KRIS"
|
||
end
|
||
return "SPRITE_CHRIS"
|
||
end
|
||
|
||
local function daytimeFor(game, mapDef)
|
||
local world = game and (game.overworld or game.world)
|
||
if world and world.daytime then return world.daytime end
|
||
local ok, Palettes = pcall(require, "src.world.gen2.Palettes")
|
||
if ok and Palettes and Palettes.daytimeFor and world and world.hour then
|
||
local hour = type(world.hour) == "function" and world:hour() or 12
|
||
return Palettes.daytimeFor(mapDef, hour, world.flashUsed)
|
||
end
|
||
return "DAY"
|
||
end
|
||
|
||
local function getSpriteRenderer(game, spriteName, seed, objDef, daytime)
|
||
local palKey = tostring(daytime or "DAY")
|
||
local key = tostring(spriteName) .. ":" .. tostring(seed) .. ":" .. palKey
|
||
local cached = FieldView._spriteCache[key]
|
||
if cached then return cached end
|
||
|
||
local data = game and game.data
|
||
local sprites = data and (data.gen2Sprites or data.sprites)
|
||
local def = sprites and sprites[spriteName]
|
||
if not def or not def.image then
|
||
return nil
|
||
end
|
||
|
||
local ok, SpriteRenderer = pcall(require, "src.render.SpriteRenderer")
|
||
if not ok or not SpriteRenderer then return nil end
|
||
local sr = SpriteRenderer.new(def, seed or spriteName)
|
||
|
||
local okP, Palettes = pcall(require, "src.world.gen2.Palettes")
|
||
local pals = data and (data.gen2Palettes or data.palettes)
|
||
if okP and Palettes and pals and sr.setObjPalette then
|
||
local colors = Palettes.spritePalette(pals, daytime or "DAY", def, objDef)
|
||
if colors then
|
||
local id = (Palettes.objectPaletteId and Palettes.objectPaletteId(objDef))
|
||
or def.paletteId or spriteName
|
||
sr:setObjPalette(colors, "game3:" .. palKey .. ":" .. tostring(id))
|
||
end
|
||
end
|
||
|
||
FieldView._spriteCache[key] = sr
|
||
return sr
|
||
end
|
||
|
||
local function objectVisible(obj)
|
||
local flag = obj.flag
|
||
if not flag or flag == 0 then return true end
|
||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||
if Space and Space.objectVisible then
|
||
return Space.objectVisible(obj)
|
||
end
|
||
return true
|
||
end
|
||
|
||
--- Prefer live host entities (animated + already palette-baked). Returns true if drawn.
|
||
local function drawWorldEntities(world, camX, camY)
|
||
local entities = world and world.entities
|
||
if type(entities) ~= "table" or #entities == 0 then return false end
|
||
|
||
local PlayerMod = package.loaded["src.core.game3.player"]
|
||
local list = {}
|
||
for _, e in ipairs(entities) do
|
||
local isPlayer = (world and world.player and e == world.player) or (e.isPlayer == true) or (e.id == "player")
|
||
if isPlayer then
|
||
if (PlayerMod and PlayerMod.isVisible and not PlayerMod.isVisible()) or (e.visible == false) or (e.hidden == true) then
|
||
-- Skip hidden player entity
|
||
elseif e and e.sprite then
|
||
list[#list + 1] = e
|
||
end
|
||
elseif e and e.sprite and (e.visible ~= false) and (e.hidden ~= true) then
|
||
list[#list + 1] = e
|
||
end
|
||
end
|
||
if #list == 0 then return false end
|
||
|
||
table.sort(list, function(a, b)
|
||
local ay = a.py or ((a.cellY or 0) * CELL)
|
||
local by = b.py or ((b.cellY or 0) * CELL)
|
||
if ay == by then
|
||
return tostring(a.id or "") < tostring(b.id or "")
|
||
end
|
||
return ay < by
|
||
end)
|
||
|
||
-- Ensure OBJ palettes are current (World normally does this; re-apply cheaply).
|
||
if world.applySpritePalette then
|
||
for _, e in ipairs(list) do
|
||
world:applySpritePalette(e)
|
||
end
|
||
end
|
||
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
for _, e in ipairs(list) do
|
||
local px = e.px or ((e.cellX or 0) * CELL)
|
||
local py = e.py or ((e.cellY or 0) * CELL)
|
||
local facing = e.facing or "down"
|
||
local phase = 0
|
||
if type(e.walkPhase) == "function" then
|
||
phase = e:walkPhase() or 0
|
||
end
|
||
local flip = e.stepFlip
|
||
if type(e.drawFlip) == "function" then
|
||
flip = e:drawFlip()
|
||
end
|
||
e.sprite:draw(px, py, camX, camY, facing, phase, flip)
|
||
end
|
||
return true
|
||
end
|
||
|
||
local function neighborActorDefs(mapId, def)
|
||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||
local ev = Space and Space.bundle and Space.bundle.events
|
||
and Space.bundle.events[mapId]
|
||
local defs = ev and (ev.objects or ev.objectEvents)
|
||
if type(defs) ~= "table" then defs = def and def.objects end
|
||
return type(defs) == "table" and defs or nil
|
||
end
|
||
|
||
local function collectNeighborActors(actors, baseIndex, hostMapId, hostDef)
|
||
local Map = package.loaded["src.core.game3.map"]
|
||
if not (Map and type(Map.world) == "table") then return baseIndex end
|
||
local Ghosts = package.loaded["src.core.game3.ghosts"]
|
||
local Objects = package.loaded["src.core.game3.objects"]
|
||
for _, entry in ipairs(Map.world) do
|
||
if entry.id ~= hostMapId and entry.def ~= hostDef then
|
||
local live = Ghosts and Ghosts.forDraw and Ghosts.forDraw(entry.id)
|
||
if live then
|
||
for _, eo in ipairs(live) do
|
||
baseIndex = baseIndex + 1
|
||
actors[#actors + 1] = {
|
||
kind = "npc",
|
||
i = baseIndex,
|
||
obj = eo.def,
|
||
ghost = entry.id,
|
||
x = (eo.px or (eo.cellX or 0) * CELL) + entry.ox * CELL,
|
||
y = (eo.py or (eo.cellY or 0) * CELL) + entry.oy * CELL,
|
||
facing = eo.facing or "down",
|
||
walkPhase = Objects and Objects.walkPhase and Objects.walkPhase(eo) or 0,
|
||
stepFlip = eo.stepFlip and true or false,
|
||
sprite = eo.sprite or spriteNameForObj(eo.def or {}),
|
||
graphicsId = eo.graphicsId
|
||
or (eo.def and (eo.def.graphicsId or eo.def.graphics)),
|
||
}
|
||
end
|
||
else
|
||
local defs = neighborActorDefs(entry.id, entry.def)
|
||
local bounds = Objects and Objects.layoutBounds
|
||
and Objects.layoutBounds(entry.def) or nil
|
||
if defs then
|
||
for _, obj in ipairs(defs) do
|
||
local ox, oy = tonumber(obj.x) or 0, tonumber(obj.y) or 0
|
||
local out = bounds and (ox < 0 or oy < 0
|
||
or ox >= bounds.w or oy >= bounds.h)
|
||
if objectVisible(obj) and not out then
|
||
baseIndex = baseIndex + 1
|
||
actors[#actors + 1] = {
|
||
kind = "npc",
|
||
i = baseIndex,
|
||
obj = obj,
|
||
ghost = entry.id,
|
||
x = (entry.ox + (tonumber(obj.x) or 0)) * CELL,
|
||
y = (entry.oy + (tonumber(obj.y) or 0)) * CELL,
|
||
facing = facingFromObj(obj),
|
||
sprite = spriteNameForObj(obj),
|
||
graphicsId = obj.graphicsId or obj.graphics,
|
||
}
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return baseIndex
|
||
end
|
||
|
||
local function pushBillboard(x, y, camX, camY)
|
||
local bb = FieldView._billboard
|
||
if not bb then return false end
|
||
local Tilt = require("src.render.Tilt")
|
||
local fx = (x - camX) + CELL / 2
|
||
local fy = (y - camY) + CELL
|
||
local sx, sy = Tilt.groundPoint(fx, fy, bb.vw, bb.vh)
|
||
love.graphics.push()
|
||
love.graphics.translate(sx - fx, sy - fy)
|
||
return true
|
||
end
|
||
|
||
local function isPlayerAboveBg2(playerXOff, playerYOff)
|
||
if (playerXOff and playerXOff ~= 0) or (playerYOff and playerYOff ~= 0) then
|
||
return true
|
||
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)
|
||
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()
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
local billboarded = pushBillboard(a.x, a.y, camX, camY)
|
||
local drew = false
|
||
if useOw and a.graphicsId ~= nil then
|
||
local opts = {
|
||
bow = a.bow,
|
||
fieldMove = a.fieldMove,
|
||
frame = a.frame,
|
||
}
|
||
drew = OwSprites.draw(
|
||
a.graphicsId, a.x, a.y, camX, camY, a.facing, a.walkPhase, a.stepFlip, opts)
|
||
end
|
||
if not drew then
|
||
local sr = getSpriteRenderer(
|
||
game, a.sprite, a.kind .. ":" .. tostring(a.i or "p"), a.obj, daytime)
|
||
if sr then
|
||
sr:draw(a.x, a.y, camX, camY, a.facing, a.walkPhase or 0, a.stepFlip)
|
||
else
|
||
local sx, sy = a.x - camX, a.y - camY
|
||
if a.kind == "player" then
|
||
love.graphics.setColor(0.95, 0.25, 0.25, 1)
|
||
else
|
||
love.graphics.setColor(0.3, 0.55, 0.95, 1)
|
||
end
|
||
love.graphics.rectangle("fill", sx + 4, sy + 2, 8, 12)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
end
|
||
if billboarded then love.graphics.pop() end
|
||
end
|
||
|
||
local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walkPhase, stepFlip, playerYOff, playerXOff)
|
||
local okO, Objects = pcall(require, "src.core.game3.objects")
|
||
local okOw, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||
local useOw = okOw and OwSprites and OwSprites.ready and OwSprites.ready()
|
||
local actors = {}
|
||
local hasObjects = okO and Objects and Objects.hasMap and Objects.hasMap()
|
||
|
||
if hasObjects then
|
||
for _, eo in ipairs(Objects.forDraw()) do
|
||
local sortY = eo.py or (eo.cellY * CELL)
|
||
if eo.moving and eo.targetY and eo.targetY > (eo.cellY or 0) then
|
||
sortY = math.max(sortY, eo.targetY * CELL)
|
||
end
|
||
actors[#actors + 1] = {
|
||
kind = "npc",
|
||
i = eo.localId,
|
||
obj = eo.def,
|
||
x = (eo.px or (eo.cellX * CELL)) + (eo.raiseX or 0),
|
||
y = (eo.py or (eo.cellY * CELL)) + (eo.raiseY or 0),
|
||
sortY = sortY,
|
||
facing = eo.facing or "down",
|
||
walkPhase = Objects.walkPhase(eo),
|
||
stepFlip = eo.stepFlip and true or false,
|
||
bow = (eo.bowFrames and eo.bowFrames > 0) or eo.raiseHand == true,
|
||
frame = eo.customFrame,
|
||
sprite = eo.sprite or spriteNameForObj(eo.def or {}),
|
||
graphicsId = eo.graphicsId or (eo.def and (eo.def.graphicsId or eo.def.graphics)),
|
||
}
|
||
end
|
||
collectNeighborActors(actors, 10000, currentMapId(game),
|
||
resolveMapDef(game, currentMapId(game)))
|
||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||
if Space and Space.resolveObjectGraphicsId then
|
||
for _, a in ipairs(actors) do
|
||
if a.obj and not a.ghost then
|
||
local gid = Space.resolveObjectGraphicsId(a.obj)
|
||
if gid then a.graphicsId = gid end
|
||
end
|
||
end
|
||
end
|
||
else
|
||
local world = game and (game.overworld or game.world)
|
||
if world and world.npcs then
|
||
for i, npc in ipairs(world.npcs) do
|
||
actors[#actors + 1] = {
|
||
kind = "npc",
|
||
i = i,
|
||
obj = npc.def,
|
||
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),
|
||
facing = npc.facing or "down",
|
||
sprite = spriteNameForObj(npc.def or {}),
|
||
graphicsId = useOw and OwSprites.playerGraphicsId and npc.graphicsId or nil,
|
||
}
|
||
end
|
||
elseif type(mapDef.objects) == "table" then
|
||
for i, obj in ipairs(mapDef.objects) do
|
||
if objectVisible(obj) then
|
||
actors[#actors + 1] = {
|
||
kind = "npc",
|
||
i = i,
|
||
obj = obj,
|
||
x = (tonumber(obj.x) or 0) * CELL,
|
||
y = (tonumber(obj.y) or 0) * CELL,
|
||
sortY = (tonumber(obj.y) or 0) * CELL,
|
||
facing = facingFromObj(obj),
|
||
sprite = spriteNameForObj(obj),
|
||
graphicsId = obj.graphicsId or obj.graphics,
|
||
}
|
||
end
|
||
end
|
||
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
|
||
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",
|
||
x = px + (playerXOff or 0),
|
||
y = py + (playerYOff or 0),
|
||
sortY = playerSortY,
|
||
facing = facing or "down",
|
||
walkPhase = (walkPhase == 1 or walkPhase == true) and 1 or 0,
|
||
stepFlip = stepFlip and true or false,
|
||
fieldMove = (PlayerMod and PlayerMod.fieldMoveAnim and PlayerMod.fieldMoveAnim > 0),
|
||
sprite = playerSpriteName(game),
|
||
graphicsId = useOw and OwSprites.playerGraphicsId(game) or nil,
|
||
}
|
||
end
|
||
|
||
table.sort(actors, function(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
|
||
|
||
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
|
||
end
|
||
|
||
--- Collect visible tile draws grouped by palette slot for batched GbcPalette.with.
|
||
local function collectTileDraws(mapDef, camX, camY, canvasW, canvasH)
|
||
local bySlot = {} -- slot → { {quad, x, y}, ... }
|
||
local blocksTbl = FieldView._blockTiles
|
||
local tilePals = FieldView._tilePalettes
|
||
local bx0 = math.floor(camX / BLOCK) - 1
|
||
local by0 = math.floor(camY / BLOCK) - 1
|
||
local bx1 = math.floor((camX + canvasW) / BLOCK) + 1
|
||
local by1 = math.floor((camY + canvasH) / BLOCK) + 1
|
||
|
||
for by = by0, by1 do
|
||
for bx = bx0, bx1 do
|
||
local blockId = blockIdAt(mapDef, bx, by)
|
||
local block = blocksTbl[(blockId or 0) + 1]
|
||
if type(block) == "table" then
|
||
local tiles = block.tiles or block
|
||
local originX = bx * BLOCK - camX
|
||
local originY = by * BLOCK - camY
|
||
for i = 0, 15 do
|
||
local tile = tiles[i + 1] or 0
|
||
local q = quadFor(tile)
|
||
if q then
|
||
local slot = (tilePals and tilePals[tile + 1]) or 1
|
||
local list = bySlot[slot]
|
||
if not list then
|
||
list = {}
|
||
bySlot[slot] = list
|
||
end
|
||
list[#list + 1] = {
|
||
q = q,
|
||
x = originX + (i % 4) * 8,
|
||
y = originY + math.floor(i / 4) * 8,
|
||
}
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return bySlot
|
||
end
|
||
|
||
local function drawTilesColored(atlas, bySlot, bgSet)
|
||
local ok, GbcPalette = pcall(require, "src.render.GbcPalette")
|
||
local usePal = ok and GbcPalette and GbcPalette.available and GbcPalette.available()
|
||
and bgSet and GbcPalette.with
|
||
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
if usePal then
|
||
if not FieldView._loggedPal then
|
||
log("tile palettes ON (GbcPalette.with per BG slot)")
|
||
FieldView._loggedPal = true
|
||
end
|
||
for slot, list in pairs(bySlot) do
|
||
local colors = bgSet[slot] or bgSet[1]
|
||
if colors then
|
||
GbcPalette.with(colors, function()
|
||
for _, d in ipairs(list) do
|
||
love.graphics.draw(atlas, d.q, d.x, d.y)
|
||
end
|
||
end)
|
||
else
|
||
for _, d in ipairs(list) do
|
||
love.graphics.draw(atlas, d.q, d.x, d.y)
|
||
end
|
||
end
|
||
end
|
||
else
|
||
if not FieldView._loggedPal then
|
||
log("tile palettes OFF (unshaded atlas blit)")
|
||
FieldView._loggedPal = true
|
||
end
|
||
for _, list in pairs(bySlot) do
|
||
for _, d in ipairs(list) do
|
||
love.graphics.draw(atlas, d.q, d.x, d.y)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local function washColor(bgSet)
|
||
if bgSet and bgSet[1] and bgSet[1][1] then
|
||
local c = bgSet[1][1]
|
||
return (c[1] or 40) / 255, (c[2] or 80) / 255, (c[3] or 60) / 255
|
||
end
|
||
return 0.12, 0.28, 0.22
|
||
end
|
||
|
||
local function ensure_batch(store, srcPair, texture, capacity)
|
||
local batch = store[srcPair]
|
||
if not batch then
|
||
batch = love.graphics.newSpriteBatch(texture, capacity)
|
||
store[srcPair] = batch
|
||
elseif batch.getTexture and batch:getTexture() ~= texture then
|
||
batch = love.graphics.newSpriteBatch(texture, capacity)
|
||
store[srcPair] = batch
|
||
end
|
||
return batch
|
||
end
|
||
|
||
--- Prefer native mid atlas when ready. Supports cross-pair connection seams
|
||
-- (e.g. Route1 pallet_outdoor → Viridian viridian_outdoor) by batching per pair.
|
||
-- Draws under-layer (BG3/BG1) only; call drawNativeOverTiles after sprites for BG2.
|
||
local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||
if not Versions.NATIVE_RENDER then return false end
|
||
local layout = mapDef.midLayout
|
||
local pair = mapDef.pair or (layout and layout.pair)
|
||
if not layout or not pair then return false end
|
||
|
||
local okN, NativeTileset = pcall(require, "src.core.game3.tileset_native")
|
||
if not (okN and NativeTileset and NativeTileset.ready(pair)) then
|
||
if not FieldView._loggedNativeFallback then
|
||
log("native unavailable for " .. tostring(pair) .. " — Gen2 atlas fallback")
|
||
FieldView._loggedNativeFallback = true
|
||
end
|
||
return false
|
||
end
|
||
if not NativeTileset.get(pair) then return false end
|
||
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
love.graphics.rectangle("fill", 0, 0, canvasW, canvasH)
|
||
|
||
local cols = math.ceil(canvasW / CELL) + 3
|
||
local rows = math.ceil(canvasH / CELL) + 3
|
||
local cx0 = math.floor(camX / CELL) - 1
|
||
local cy0 = math.floor(camY / CELL) - 1
|
||
local capacity = cols * rows
|
||
|
||
FieldView._nativeBatches = FieldView._nativeBatches or {}
|
||
FieldView._nativeOverBatches = FieldView._nativeOverBatches or {}
|
||
|
||
local Map = package.loaded["src.core.game3.map"]
|
||
or require("src.core.game3.map")
|
||
local VoidFill = require("src.core.game3.void_fill")
|
||
local voidMode = VoidFill.normalize(VoidFill.mode)
|
||
|
||
if FieldView._nativeDirty
|
||
or FieldView._nativeVoid ~= voidMode
|
||
or FieldView._nativeBx ~= cx0
|
||
or FieldView._nativeBy ~= cy0
|
||
or FieldView._nativePair ~= pair then
|
||
for _, batch in pairs(FieldView._nativeBatches) do
|
||
batch:clear()
|
||
end
|
||
for _, batch in pairs(FieldView._nativeOverBatches) do
|
||
batch:clear()
|
||
end
|
||
FieldView._nativeOverByRow = {}
|
||
for r = 0, rows - 1 do
|
||
FieldView._nativeOverByRow[r] = {}
|
||
end
|
||
local cellsByPair = {}
|
||
local voidHas, voidPrimary = nil, nil
|
||
if voidMode ~= "map" and voidMode ~= "black" then
|
||
voidHas = function(m) return NativeTileset.hasMid(pair, m) end
|
||
voidPrimary = VoidFill.primaryFor(pair)
|
||
end
|
||
for row = 0, rows - 1 do
|
||
for col = 0, cols - 1 do
|
||
local mid, srcPair, isVoid = layout:midAt(cx0 + col, cy0 + row), pair, false
|
||
if Map.worldMidAt then
|
||
mid, srcPair, isVoid = Map.worldMidAt(cx0 + col, cy0 + row, mapDef)
|
||
srcPair = srcPair or pair
|
||
end
|
||
local skip = false
|
||
if isVoid and voidMode ~= "map" then
|
||
local fill = VoidFill.fillAt(voidMode, cx0 + col, cy0 + row, voidHas, voidPrimary)
|
||
if fill == false then
|
||
skip = true
|
||
elseif fill then
|
||
mid, srcPair = fill, pair
|
||
end
|
||
end
|
||
if not NativeTileset.ready(srcPair) then
|
||
srcPair = pair
|
||
end
|
||
if not skip then
|
||
local list = cellsByPair[srcPair]
|
||
if not list then
|
||
list = {}
|
||
cellsByPair[srcPair] = list
|
||
end
|
||
list[#list + 1] = { mid = mid, x = col * CELL, y = row * CELL, row = row }
|
||
end
|
||
end
|
||
end
|
||
for srcPair, cells in pairs(cellsByPair) do
|
||
local ts = NativeTileset.get(srcPair)
|
||
if ts and ts.image then
|
||
local batch = ensure_batch(FieldView._nativeBatches, srcPair, ts.image, capacity)
|
||
batch:clear()
|
||
local overBatch = nil
|
||
if ts.layered and ts.overImage then
|
||
overBatch = ensure_batch(
|
||
FieldView._nativeOverBatches, srcPair, ts.overImage, capacity)
|
||
overBatch:clear()
|
||
end
|
||
for _, cell in ipairs(cells) do
|
||
local slot = NativeTileset.slotFor(ts, cell.mid)
|
||
local q = NativeTileset.quad(ts, slot)
|
||
if q then batch:add(q, cell.x, cell.y) end
|
||
if overBatch then
|
||
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
|
||
end
|
||
end
|
||
FieldView._nativeBx = cx0
|
||
FieldView._nativeBy = cy0
|
||
FieldView._nativePair = pair
|
||
FieldView._nativeVoid = voidMode
|
||
FieldView._nativeDirty = false
|
||
end
|
||
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
local ox = -(camX % CELL) - CELL
|
||
local oy = -(camY % CELL) - CELL
|
||
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
|
||
for srcPair, batch in pairs(FieldView._nativeBatches) do
|
||
if srcPair ~= pair then
|
||
love.graphics.draw(batch, ox, oy)
|
||
end
|
||
end
|
||
|
||
if not FieldView._loggedNative then
|
||
log("native mid atlas ON pair=" .. tostring(pair))
|
||
FieldView._loggedNative = true
|
||
end
|
||
return true
|
||
end
|
||
|
||
--- BG2 overhead (building eaves, desk tops) — draw after OW sprites.
|
||
local function drawNativeOverTiles()
|
||
local batches = FieldView._nativeOverBatches
|
||
if not batches then return end
|
||
local pair = FieldView._nativeOverPair
|
||
local ox = FieldView._nativeOverOx or 0
|
||
local oy = FieldView._nativeOverOy or 0
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
if pair and batches[pair] then
|
||
love.graphics.draw(batches[pair], ox, oy)
|
||
end
|
||
for srcPair, batch in pairs(batches) do
|
||
if srcPair ~= pair then
|
||
love.graphics.draw(batch, ox, oy)
|
||
end
|
||
end
|
||
end
|
||
|
||
function FieldView.radiusForLevel(level)
|
||
level = tonumber(level) or 0
|
||
return FLASH_LEVEL_RADIUS[level] or FLASH_LEVEL_RADIUS[0]
|
||
end
|
||
|
||
-- pokefirered/src/overworld.c:966
|
||
function FieldView.setFlashLevel(level)
|
||
level = tonumber(level) or 0
|
||
if level < 0 or level > FieldView.MAX_FLASH_LEVEL then level = 0 end
|
||
FieldView.flashLevel = level
|
||
FieldView._flashRadius = nil
|
||
-- pokefirered/include/global.h:770 gSaveBlock1Ptr->flashLevel
|
||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||
if session then session.flashLevel = level end
|
||
end
|
||
|
||
-- pokefirered/src/overworld.c:973
|
||
function FieldView.getFlashLevel()
|
||
return FieldView.flashLevel
|
||
end
|
||
|
||
function FieldView.setFlashRadius(radius)
|
||
FieldView._flashRadius = tonumber(radius)
|
||
end
|
||
|
||
-- pokefirered/src/field_screen_effect.c:194
|
||
function FieldView.animateFlashLevel(fromLevel, toLevel)
|
||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||
if okFx and FieldEffects and FieldEffects.animateFlashLevel then
|
||
return FieldEffects.animateFlashLevel(fromLevel, toLevel)
|
||
end
|
||
FieldView.setFlashLevel(toLevel)
|
||
return nil
|
||
end
|
||
|
||
-- pokefirered/src/overworld.c:1756
|
||
function FieldView.flashRadius()
|
||
if FieldView._flashRadius then return FieldView._flashRadius end
|
||
if FieldView.flashLevel == 0 then return nil end
|
||
return FieldView.radiusForLevel(FieldView.flashLevel)
|
||
end
|
||
|
||
-- pokefirered/src/field_camera.c:507
|
||
function FieldView.setCameraPanning(x, y)
|
||
FieldView.cameraPanX = tonumber(x) or 0
|
||
FieldView.cameraPanY = tonumber(y) or 0
|
||
end
|
||
|
||
local function flashActive()
|
||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||
if Space and Space.store then
|
||
local okF, Flags = pcall(require, "src.core.game3.scripting.flags")
|
||
if okF and Flags and Flags.getFlag
|
||
and Flags.getFlag(Space.store, nil, FLAG_SYS_FLASH_ACTIVE) then
|
||
return true
|
||
end
|
||
end
|
||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||
local flags = session and session.flags
|
||
return (flags and flags[FLAG_SYS_FLASH_ACTIVE]) and true or false
|
||
end
|
||
|
||
-- pokefirered/src/overworld.c:958
|
||
local function mapIsCave(def)
|
||
if def and def.cave ~= nil then return (tonumber(def.cave) or 0) ~= 0 end
|
||
return false
|
||
end
|
||
|
||
-- pokefirered/src/overworld.c:956
|
||
function FieldView.defaultFlashLevel(game, mapId)
|
||
if not mapIsCave(resolveMapDef(game, mapId)) then return 0 end
|
||
if flashActive() then return 0 end
|
||
return FieldView.MAX_FLASH_LEVEL
|
||
end
|
||
|
||
function FieldView.setDefaultFlashLevel(game, mapId)
|
||
FieldView._flashMapId = mapId
|
||
FieldView.setFlashLevel(FieldView.defaultFlashLevel(game, mapId))
|
||
return FieldView.flashLevel
|
||
end
|
||
|
||
-- pokefirered/src/field_screen_effect.c:90
|
||
local function flashWindowRows(centerX, centerY, radius, w, h)
|
||
local rows = {}
|
||
local maxX = 255
|
||
if w > maxX then maxX = w end
|
||
local function put(y, left, right)
|
||
if y >= 0 and y <= h then
|
||
if left < 0 then left = 0 elseif left > maxX then left = maxX end
|
||
if right < 0 then right = 0 elseif right > maxX then right = maxX end
|
||
rows[y] = { left, right }
|
||
end
|
||
end
|
||
local xy, err, yx = radius, radius, 0
|
||
while xy >= yx do
|
||
put(centerY - yx, centerX - xy, centerX + xy)
|
||
put(centerY + yx, centerX - xy, centerX + xy)
|
||
put(centerY - xy, centerX - yx, centerX + yx)
|
||
put(centerY + xy, centerX - yx, centerX + yx)
|
||
err = err - ((yx * 2) - 1)
|
||
yx = yx + 1
|
||
if err < 0 then
|
||
err = err + 2 * (xy - 1)
|
||
xy = xy - 1
|
||
end
|
||
end
|
||
return rows
|
||
end
|
||
|
||
-- pokefirered/src/field_screen_effect.c:37
|
||
function FieldView.flashSpans(radius, w, h, centerX, centerY)
|
||
w = math.floor(tonumber(w) or Display.W)
|
||
h = math.floor(tonumber(h) or Display.H)
|
||
centerX = math.floor(centerX or (w / 2))
|
||
centerY = math.floor(centerY or (h / 2))
|
||
local rows = flashWindowRows(centerX, centerY, math.floor(radius), w, h)
|
||
local spans = {}
|
||
local y = 0
|
||
while y < h do
|
||
local r = rows[y]
|
||
local left = r and r[1] or 0
|
||
local right = r and r[2] or 0
|
||
local y2 = y + 1
|
||
while y2 < h do
|
||
local n = rows[y2]
|
||
if (n and n[1] or 0) ~= left or (n and n[2] or 0) ~= right then break end
|
||
y2 = y2 + 1
|
||
end
|
||
spans[#spans + 1] = { y = y, height = y2 - y, left = left, right = right }
|
||
y = y2
|
||
end
|
||
return spans
|
||
end
|
||
|
||
function FieldView.flashSpansFor(radius, w, h, cx, cy)
|
||
local spans = FieldView._flashSpans
|
||
if spans
|
||
and FieldView._flashSpanR == radius
|
||
and FieldView._flashSpanCX == cx and FieldView._flashSpanCY == cy
|
||
and FieldView._flashSpanW == w and FieldView._flashSpanH == h then
|
||
return spans
|
||
end
|
||
spans = FieldView.flashSpans(radius, w, h, cx, cy)
|
||
FieldView._flashSpans = spans
|
||
FieldView._flashSpanR = radius
|
||
FieldView._flashSpanCX = cx
|
||
FieldView._flashSpanCY = cy
|
||
FieldView._flashSpanW = w
|
||
FieldView._flashSpanH = h
|
||
return spans
|
||
end
|
||
|
||
-- pokefirered/src/overworld.c:2077
|
||
local function drawFlashMask(w, h)
|
||
local radius = FieldView.flashRadius()
|
||
if not radius then return end
|
||
local cx, cy = math.floor(w / 2), math.floor(h / 2)
|
||
local spans = FieldView.flashSpansFor(radius, w, h, cx, cy)
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
for i = 1, #spans do
|
||
local s = spans[i]
|
||
if s.left > 0 then
|
||
love.graphics.rectangle("fill", 0, s.y, s.left, s.height)
|
||
end
|
||
if s.right < w then
|
||
love.graphics.rectangle("fill", s.right, s.y, w - s.right, s.height)
|
||
end
|
||
end
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
function FieldView.draw(game, canvasW, canvasH, opts)
|
||
canvasW = canvasW or Display.W
|
||
canvasH = canvasH or Display.H
|
||
opts = opts or {}
|
||
|
||
local mapId = currentMapId(game)
|
||
local mapDef = resolveMapDef(game, mapId)
|
||
if FieldView._flashMapId ~= mapId then
|
||
FieldView.setDefaultFlashLevel(game, mapId)
|
||
end
|
||
if not mapDef or (not mapDef.blocks and not mapDef.midLayout) or not mapDef.width then
|
||
love.graphics.setColor(0.2, 0.35, 0.55, 1)
|
||
love.graphics.rectangle("fill", 0, 0, canvasW, canvasH)
|
||
love.graphics.print("game3: no layout " .. tostring(mapId), 8, 8)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
return
|
||
end
|
||
|
||
local px, py, facing, walkPhase, stepFlip, _, playerYOff, playerXOff = playerPixels(game)
|
||
playerXOff = playerXOff or 0
|
||
playerYOff = playerYOff or 0
|
||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||
local session = Runtime and Runtime.getSession and Runtime.getSession()
|
||
local world = game and (game.overworld or game.world)
|
||
if session then
|
||
session.x = math.floor((px or 0) / CELL)
|
||
session.y = math.floor((py or 0) / CELL)
|
||
session.facing = facing
|
||
if mapId then session.map = mapId end
|
||
end
|
||
|
||
-- pret CameraUpdate: always track the player. No map-rect clamp — edges
|
||
-- show connected neighbors (or border), same as walking mid-town.
|
||
local camX = math.floor(px + CELL / 2 - canvasW / 2)
|
||
local camY = math.floor(py + CELL / 2 - canvasH / 2)
|
||
-- pokefirered/src/field_camera.c:89
|
||
camX = camX + (FieldView.cameraPanX or 0)
|
||
camY = camY + (FieldView.cameraPanY or 0)
|
||
|
||
local screenOx = math.floor((canvasW - Display.W) / 2)
|
||
local screenOy = math.floor((canvasH - Display.H) / 2)
|
||
|
||
-- pret BuyMenuDrawMapBg (shop.c:731-734): Frame player & counter in left gap (X: 0..80, Y: 0..160)
|
||
local okShop, ShopMenu = pcall(require, "src.ui.game3.shop_menu")
|
||
if okShop and ShopMenu and ShopMenu.isShopCamera and ShopMenu.isShopCamera() then
|
||
local fx, fy = px, py
|
||
if facing == "up" or facing == "north" then fy = fy - CELL
|
||
elseif facing == "down" or facing == "south" then fy = fy + CELL
|
||
elseif facing == "left" or facing == "west" then fx = fx - CELL
|
||
elseif facing == "right" or facing == "east" then fx = fx + CELL
|
||
else fx = fx - CELL end
|
||
local fTileX = math.floor(fx / CELL)
|
||
local fTileY = math.floor(fy / CELL)
|
||
camX = (fTileX - 2) * CELL
|
||
camY = (fTileY - 4) * CELL
|
||
end
|
||
|
||
FieldView._billboard = opts.billboard
|
||
and { vw = canvasW, vh = canvasH } or nil
|
||
|
||
if not opts.actorsOnly then
|
||
local Map = package.loaded["src.core.game3.map"]
|
||
or require("src.core.game3.map")
|
||
if Map.refreshWorld then
|
||
Map.refreshWorld(game, math.ceil(canvasW / CELL), math.ceil(canvasH / CELL), mapId)
|
||
end
|
||
end
|
||
|
||
local usedNative = FieldView._nativeOverPair ~= nil
|
||
if not opts.actorsOnly then
|
||
usedNative = drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||
if usedNative then
|
||
-- Native batch already covers viewport (+ overscan); wash skipped.
|
||
else
|
||
local tileset = resolveTileset(game, mapDef)
|
||
local atlas = loadAtlas(tileset)
|
||
if not atlas or not FieldView._blockTiles then
|
||
love.graphics.setColor(0.45, 0.2, 0.2, 1)
|
||
love.graphics.rectangle("fill", 0, 0, canvasW, canvasH)
|
||
love.graphics.print("game3: no tileset atlas", 8, 8)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
return
|
||
end
|
||
|
||
local bgSet = resolveBgSet(game, mapDef, daytimeFor(game, mapDef))
|
||
local wr, wg, wb = washColor(bgSet)
|
||
love.graphics.setColor(wr, wg, wb, 1)
|
||
love.graphics.rectangle("fill", 0, 0, canvasW, canvasH)
|
||
|
||
local bySlot = collectTileDraws(mapDef, camX, camY, canvasW, canvasH)
|
||
drawTilesColored(atlas, bySlot, bgSet)
|
||
end
|
||
|
||
-- Tall grass under body (pret lower OAM priority).
|
||
do
|
||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||
if okFx and FieldEffects and FieldEffects.drawBehind then
|
||
FieldEffects.drawBehind(camX, camY)
|
||
end
|
||
end
|
||
|
||
-- Door opening/closing animation overlays (under actors).
|
||
do
|
||
local okDoors, Doors = pcall(require, "src.core.game3.doors")
|
||
if okDoors and Doors and Doors.draw then
|
||
Doors.draw(camX, camY, canvasW, canvasH)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- pokefirered/src/field_effect.c:910
|
||
if not opts.actorsOnly then
|
||
local okHeal, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||
if okHeal and Heal and Heal.drawBalls then
|
||
local sx, sy = screenAnchor(px, py, camX, camY)
|
||
love.graphics.push()
|
||
love.graphics.translate(sx, sy)
|
||
Heal.drawBalls(camX, camY)
|
||
love.graphics.pop()
|
||
end
|
||
end
|
||
|
||
-- pokefirered/src/field_effect.c:3946: the Deoxys shatter blends only the BG
|
||
-- palettes to white, so the map washes out while the rock fragments (OBJ
|
||
-- sprites) keep their colours. Painted here, between the last map layer and
|
||
-- the actors, for exactly that reason -- a Renderer.screenVeil would cover
|
||
-- the fragments too and the shatter would be invisible.
|
||
if not opts.actorsOnly then
|
||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||
if okFx and FieldEffects and FieldEffects.bgFlashAlpha then
|
||
local a = FieldEffects.bgFlashAlpha()
|
||
if a and a > 0 then
|
||
love.graphics.setColor(1, 1, 1, a)
|
||
love.graphics.rectangle("fill", 0, 0, canvasW, canvasH)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Draw Game3 actors and native overhead tiles interleaved by Y-row.
|
||
if not opts.skipActors then
|
||
local actors = collectGame3Actors(
|
||
game, mapDef, camX, camY, px, py, facing, walkPhase, stepFlip, playerYOff, playerXOff)
|
||
drawInterleaved(
|
||
game, mapDef, camX, camY, actors, FieldView._nativeRows, FieldView._nativeCy0)
|
||
end
|
||
|
||
-- Tall grass over feet (pret subpriority above avatar).
|
||
if not opts.actorsOnly then
|
||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||
if okFx and FieldEffects and FieldEffects.drawFront then
|
||
FieldEffects.drawFront(camX, camY, py)
|
||
end
|
||
end
|
||
|
||
-- pokefirered/src/field_effect.c:1024
|
||
if not opts.actorsOnly then
|
||
local okHeal, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||
if okHeal and Heal and Heal.drawMonitor then
|
||
local sx, sy = screenAnchor(px, py, camX, camY)
|
||
love.graphics.push()
|
||
love.graphics.translate(sx, sy)
|
||
Heal.drawMonitor(camX, camY)
|
||
love.graphics.pop()
|
||
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")
|
||
if okFx and FieldEffects and FieldEffects.drawOverlay then
|
||
love.graphics.push()
|
||
love.graphics.translate(screenOx, screenOy)
|
||
FieldEffects.drawOverlay(camX, camY)
|
||
love.graphics.pop()
|
||
end
|
||
end
|
||
|
||
drawFlashMask(canvasW, canvasH)
|
||
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
--- Drop cached atlases/sprites (tileset hot-reload).
|
||
function FieldView.invalidate()
|
||
FieldView._atlas = nil
|
||
FieldView._atlasPath = nil
|
||
FieldView._quads = {}
|
||
FieldView._blockTiles = nil
|
||
FieldView._tilePalettes = nil
|
||
FieldView._spriteCache = {}
|
||
FieldView._logged = false
|
||
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
|
||
FieldView._nativeOverPair = nil
|
||
FieldView._nativeDirty = true
|
||
FieldView._loggedNative = false
|
||
FieldView._loggedNativeFallback = false
|
||
FieldView._flashSpans = nil
|
||
FieldView._flashSpanR = nil
|
||
local okN, NativeTileset = pcall(require, "src.core.game3.tileset_native")
|
||
if okN and NativeTileset and NativeTileset.invalidate then
|
||
NativeTileset.invalidate()
|
||
end
|
||
local okO, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||
if okO and OwSprites and OwSprites.invalidate then
|
||
OwSprites.invalidate()
|
||
end
|
||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||
if okFx and FieldEffects and FieldEffects.invalidate then
|
||
FieldEffects.invalidate()
|
||
end
|
||
end
|
||
|
||
return FieldView
|