mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 08:11:35 +02:00
discord rich presence, and MORE COLORS, video options, shiddddd so much stuff
This commit is contained in:
+124
-2
@@ -196,6 +196,114 @@ function PaletteFX.monPalName(data, species, transformed)
|
||||
return "MEWMON"
|
||||
end
|
||||
|
||||
-- ------- true GBC overworld coloring (color/loadpalettes.asm,
|
||||
-- color/data/*, color/sprites.asm ColorOverworldSprite) -------------------
|
||||
--
|
||||
-- RED++ pairs its named-palette battle/mon colors above with pokered-gbc's
|
||||
-- real per-tile system: LoadTilesetPalette assigns one of 8 four-color BG
|
||||
-- palettes to every tile GRAPHIC in a tileset (by tile id, not by map
|
||||
-- position), and LoadTownPalette swaps just the ROOF slot (index 6) per
|
||||
-- town/route. `data/palettes_gbc.lua`'s `world` table holds the extracted
|
||||
-- data (tools/extract/palettes.py extract_gbc_world); these queries are
|
||||
-- mode-independent (only check the pack exists) so TileRenderer can
|
||||
-- precompute geometry once regardless of the active COLORS mode -- callers
|
||||
-- that resolve to actual on-screen COLOR should gate on usesGbcPack()
|
||||
-- themselves, the same way they already gate other RED++-only behavior.
|
||||
--
|
||||
-- LoadTilesetPalette's 3 hardcoded single-tile fixes (Celadon Mart) and
|
||||
-- LoadTownPalette's Route 6/Saffron y<2 roof split are control flow, not
|
||||
-- data, so they are not in the extracted pack -- they live here instead.
|
||||
local TILE_GROUP_EXCEPTIONS = {
|
||||
-- tile ids $4b-$4f -> BLUE (outside sky, seen through the mart's roof)
|
||||
CELADON_MART_ROOF = { tiles = { [0x4b] = true, [0x4c] = true, [0x4d] = true,
|
||||
[0x4e] = true, [0x4f] = true }, group = 3 },
|
||||
-- tile $37 -> BROWN (counter miscoloration fix)
|
||||
CELADON_MART_3F = { tiles = { [0x37] = true }, group = 5 },
|
||||
-- tiles $07/$08/$17/$18 -> YELLOW (bench, blue by default)
|
||||
CELADON_MART_1F = { tiles = { [0x07] = true, [0x08] = true,
|
||||
[0x17] = true, [0x18] = true }, group = 4 },
|
||||
}
|
||||
local ROOF_GROUP = 6
|
||||
local ROUTE_6_SAFFRON = { mapId = "ROUTE_6", useMapId = "SAFFRON_CITY", cellYBelow = 2 }
|
||||
|
||||
-- whether the extracted pack has real per-tile GBC data for this tileset
|
||||
-- (false for a mod tileset with no pokered-gbc counterpart, or when the
|
||||
-- pack failed to load at all)
|
||||
function PaletteFX.hasWorldTileset(tileset)
|
||||
local pack = PaletteFX.gbcPack()
|
||||
local w = pack and pack.world
|
||||
return (w and w.tileGroups[tileset]) ~= nil
|
||||
end
|
||||
|
||||
-- the palette-group (0-7) a tile GRAPHIC id resolves to in this tileset,
|
||||
-- with the current map's tile-id exceptions (if any) applied first
|
||||
function PaletteFX.worldGroupAt(tileset, mapId, tileId)
|
||||
local pack = PaletteFX.gbcPack()
|
||||
local w = pack and pack.world
|
||||
local groups = w and w.tileGroups[tileset]
|
||||
if not groups then return nil end
|
||||
local exc = TILE_GROUP_EXCEPTIONS[mapId]
|
||||
if exc and exc.tiles[tileId] then return exc.group end
|
||||
return groups[tileId] or 7 -- TEXT: tile ids past the tileset's 96 (menus)
|
||||
end
|
||||
|
||||
-- this tileset's resolved 8-entry {r,g,b}x4 palette array, with the ROOF
|
||||
-- slot swapped to the current town/route (Route 6's north end uses
|
||||
-- Saffron's roof colors while the player stands in its top 2 cell rows,
|
||||
-- like pokered's wYCoord check -- data is Game.data, for the map lookup)
|
||||
function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY)
|
||||
local pack = PaletteFX.gbcPack()
|
||||
local w = pack and pack.world
|
||||
local base = w and w.groupColors[tileset]
|
||||
if not base then return nil end
|
||||
if not w.roofGroup[tileset] then return base end
|
||||
local roofMapId = mapId
|
||||
if mapId == ROUTE_6_SAFFRON.mapId and playerCellY
|
||||
and playerCellY < ROUTE_6_SAFFRON.cellYBelow then
|
||||
roofMapId = ROUTE_6_SAFFRON.useMapId
|
||||
end
|
||||
local roofMap = data and data.maps and data.maps[roofMapId]
|
||||
local roof = roofMap and w.roofByMapIndex[roofMap.index]
|
||||
if not roof then return base end
|
||||
local out = {}
|
||||
for i = 1, 8 do out[i] = base[i] end
|
||||
-- LoadTownPalette only overwrites W2_BgPaletteData + $32, i.e. colors 1
|
||||
-- and 2 (0-indexed) of the 4-color ROOF slot -- color 0 (background,
|
||||
-- typically the sky-through-gaps white) and color 3 (outline black) keep
|
||||
-- the tileset's own OUTDOOR_ROOF/INDOOR_ROOF base, only the roof
|
||||
-- material's 2 middle shades are town-specific
|
||||
local base4 = base[ROOF_GROUP + 1]
|
||||
out[ROOF_GROUP + 1] = { base4[1], roof[1], roof[2], base4[4] }
|
||||
return out
|
||||
end
|
||||
|
||||
-- an overworld sprite's resolved 4-color OBJ palette (ColorOverworldSprite),
|
||||
-- or nil when unassigned/unavailable, plus the resolved group index (for
|
||||
-- callers that want a stable cache key without hashing the colors table).
|
||||
-- spriteDef carries the ROM picture-id crosswalk in its `source` field
|
||||
-- ("ROM:SpriteSheetPointerTable[N]"); seed (any stable per-instance value,
|
||||
-- e.g. an NPC's `id`) resolves the "random" sentinel -- a deliberate
|
||||
-- approximation of ColorOverworldSprite's per-OAM-slot pseudo-random pick
|
||||
-- (`swap a; and 3` on the sprite's OAM offset, which has no equivalent
|
||||
-- here): a stable hash instead, so the same NPC instance always shows the
|
||||
-- same one of the 4 SPR_PAL_* colors.
|
||||
function PaletteFX.spriteObp(spriteDef, seed)
|
||||
local pack = PaletteFX.gbcPack()
|
||||
local w = pack and pack.world
|
||||
local src = spriteDef and spriteDef.source
|
||||
if not (w and src) then return nil end
|
||||
local idx = tonumber(src:match("%[(%d+)%]"))
|
||||
local group = idx and w.spriteAssignment[idx]
|
||||
if group == nil then return nil end
|
||||
if group == "random" then
|
||||
local h = 0
|
||||
seed = tostring(seed or "")
|
||||
for i = 1, #seed do h = (h * 31 + seed:byte(i)) % 4294967296 end
|
||||
group = h % 4
|
||||
end
|
||||
return w.spritePalettes[group], group
|
||||
end
|
||||
|
||||
-- GetHealthBarColor (home/palettes.asm) on the standard 48px bar
|
||||
function PaletteFX.barPalName(hp, maxHp)
|
||||
local px = maxHp > 0 and math.floor(hp * 48 / maxHp) or 0
|
||||
@@ -237,10 +345,24 @@ function PaletteFX.setMode(mode)
|
||||
end
|
||||
end
|
||||
if not ok then PaletteFX.mode = "gbc" end
|
||||
-- battle pics bake the active pack into ImageData; drop the cache when
|
||||
-- the pack (or any COLORS mode) changes so the next draw re-tints
|
||||
-- battle pics and overworld sprites bake the active pack into ImageData;
|
||||
-- drop those caches when the pack (or any COLORS mode) changes so the
|
||||
-- next draw re-tints
|
||||
if prev ~= PaletteFX.mode then
|
||||
pcall(function() require("src.battle.BattleState").invalidate() end)
|
||||
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
|
||||
-- RED++'s baked tileset atlas (TileRenderer.getGbcAtlas) is built once
|
||||
-- per loaded map, so a mode toggle needs every cached Map/TileRenderer
|
||||
-- dropped and the currently-visible one rebuilt in place -- otherwise
|
||||
-- the on-screen map keeps its stale (wrong-mode) atlas until the next
|
||||
-- map transition happens to reload it.
|
||||
pcall(function()
|
||||
require("src.world.MapLoader").invalidateAll()
|
||||
local Game = require("src.core.Game")
|
||||
if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
|
||||
Game.overworld:reloadMap(Game.overworld.map.id, "colors")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,10 +18,49 @@ local function getImage(path)
|
||||
return imageCache[path]
|
||||
end
|
||||
|
||||
-- RED++ overworld sprite OBJ-palette recolor (color/sprites.asm
|
||||
-- ColorOverworldSprite), baked into an ImageData like BattleState's mon-pic
|
||||
-- palette bake (src/battle/BattleState.lua getImage): CPU-remap the 4 DMG
|
||||
-- shades to the resolved OBP colors, cached per (image path, group).
|
||||
--
|
||||
-- Sprite sheets carry no real alpha (every pixel, including the
|
||||
-- background, is opaque -- confirmed by sampling the extracted PNGs): the
|
||||
-- "transparent" look in every other draw path is a coincidence of the
|
||||
-- whole-canvas shade-remap shader, where shade 0 (white) happens to map to
|
||||
-- a similarly light color in whatever terrain zone the sprite stands over.
|
||||
-- That coincidence breaks once terrain is colored per-tile instead of one
|
||||
-- flat color per map (different tiles can have very different color-0s),
|
||||
-- so shade 0 is keyed to alpha 0 here explicitly -- matching real GBC OBJ
|
||||
-- hardware, where sprite palette index 0 is unconditionally transparent
|
||||
-- (same rule TileRenderer's getColor0KeyShader documents for tall grass).
|
||||
local obpCache = {}
|
||||
|
||||
local function getObpImage(path, colors, group)
|
||||
local key = path .. "#obp" .. group
|
||||
if not obpCache[key] then
|
||||
local img
|
||||
if love.image and love.image.newImageData then
|
||||
local id = Assets.imageData(path)
|
||||
id:mapPixel(function(_, _, r, g, b, a)
|
||||
if a == 0 then return r, g, b, a end
|
||||
if r > 0.83 then return r, g, b, 0 end -- OBJ color 0: always transparent
|
||||
local col = r > 0.5 and colors[2] or r > 0.17 and colors[3] or colors[4]
|
||||
return col[1] / 255, col[2] / 255, col[3] / 255, a
|
||||
end)
|
||||
img = love.graphics.newImage(id)
|
||||
else
|
||||
img = getImage(path) -- headless stub: no pixel access
|
||||
end
|
||||
obpCache[key] = img
|
||||
end
|
||||
return obpCache[key]
|
||||
end
|
||||
|
||||
-- hot reload drops the sheets; live instances hold their own image, so
|
||||
-- the world rebuilds them (MapLoader.invalidateAll) rather than this
|
||||
function SpriteRenderer.invalidate()
|
||||
imageCache = {}
|
||||
obpCache = {}
|
||||
end
|
||||
|
||||
Assets.register(SpriteRenderer.invalidate)
|
||||
@@ -29,9 +68,12 @@ Assets.register(SpriteRenderer.invalidate)
|
||||
local STAND = { down = 0, up = 1, left = 2, right = 2 }
|
||||
local WALK = { down = 3, up = 4, left = 5, right = 5 }
|
||||
|
||||
function SpriteRenderer.new(spriteDef)
|
||||
-- seed: any stable per-instance value (e.g. an NPC's `id`) used to resolve
|
||||
-- RED++'s per-instance "random" OBP sentinel (PaletteFX.spriteObp)
|
||||
function SpriteRenderer.new(spriteDef, seed)
|
||||
local self = setmetatable({}, SpriteRenderer)
|
||||
self.def = spriteDef
|
||||
self.seed = seed
|
||||
self.image = getImage(spriteDef.image)
|
||||
local iw, ih = self.image:getDimensions()
|
||||
self.frames = {}
|
||||
@@ -46,13 +88,26 @@ end
|
||||
function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip)
|
||||
local x = math.floor(px - camX)
|
||||
local y = math.floor(py - camY) - 4
|
||||
local image = self.image
|
||||
-- full-color art claims its 16x16 cell out of the shade-remap pass
|
||||
if self.def.trueColor then PaletteFX.markTrueColor(x, y, 16, 16) end
|
||||
if self.def.trueColor then
|
||||
PaletteFX.markTrueColor(x, y, 16, 16)
|
||||
elseif PaletteFX.usesGbcPack() then
|
||||
-- RED++: the world canvas is already true-color (TileRenderer bakes
|
||||
-- terrain, this bakes the sprite) and the world pass runs unshaded
|
||||
-- (OverworldState.sgbWorldZones), so this draws like any normal sprite
|
||||
-- -- opaque character pixels over a real-alpha-transparent background,
|
||||
-- no trueColor rect needed (there is no shader left to exempt it from).
|
||||
local colors, group = PaletteFX.spriteObp(self.def, self.seed)
|
||||
if colors then
|
||||
image = getObpImage(self.def.image, colors, group)
|
||||
end
|
||||
end
|
||||
-- single-frame sprites (item balls, fossils...) have one fixed pose;
|
||||
-- still 3-frame sprites turn to face (the nurse at her machine,
|
||||
-- facePlayer on STAY NPCs) but never show walk frames
|
||||
if self.def.frames <= 1 then
|
||||
love.graphics.draw(self.image, self.frames[0], x, y)
|
||||
love.graphics.draw(image, self.frames[0], x, y)
|
||||
return
|
||||
end
|
||||
local frame = (self.def.walker and walkPhase == 1)
|
||||
@@ -65,9 +120,9 @@ function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip)
|
||||
end
|
||||
local quad = self.frames[frame] or self.frames[0]
|
||||
if flip then
|
||||
love.graphics.draw(self.image, quad, x + 16, y, 0, -1, 1)
|
||||
love.graphics.draw(image, quad, x + 16, y, 0, -1, 1)
|
||||
else
|
||||
love.graphics.draw(self.image, quad, x, y)
|
||||
love.graphics.draw(image, quad, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+133
-12
@@ -108,10 +108,23 @@ end
|
||||
-- the water/flower branches did before they were data.
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
-- the 8 shifted variants of one tile (built once per sheet + tile id)
|
||||
-- shade 0-3 -> one of `colors`' 4 entries (same cutoffs PaletteFX's shader
|
||||
-- uses), alpha passed through unchanged; nil colors leaves r,g,b as-is.
|
||||
-- Shared by the whole-atlas bake (getGbcAtlas) and the animated-tile
|
||||
-- variants below, so water/flowers/spinners match the static tiles around
|
||||
-- them under RED++ instead of showing their un-recolored grayscale.
|
||||
local function recolorSample(r, g, b, a, colors)
|
||||
if not (colors and a > 0) then return r, g, b, a end
|
||||
local col = r > 0.83 and colors[1] or r > 0.5 and colors[2]
|
||||
or r > 0.17 and colors[3] or colors[4]
|
||||
return col[1] / 255, col[2] / 255, col[3] / 255, a
|
||||
end
|
||||
|
||||
-- the 8 shifted variants of one tile (built once per sheet + tile id [+
|
||||
-- gbcKey, when `colors` recolors it for RED++ -- see buildAnim])
|
||||
local shiftVariants = {}
|
||||
local function getShiftVariants(tilesetImagePath, perRow, tile)
|
||||
local key = tilesetImagePath .. "#" .. tile
|
||||
local function getShiftVariants(tilesetImagePath, perRow, tile, colors, gbcKey)
|
||||
local key = tilesetImagePath .. "#" .. tile .. (gbcKey or "")
|
||||
if shiftVariants[key] ~= nil then return shiftVariants[key] end
|
||||
if not (love.image and love.image.newImageData) then
|
||||
shiftVariants[key] = false
|
||||
@@ -126,6 +139,7 @@ local function getShiftVariants(tilesetImagePath, perRow, tile)
|
||||
for y = 0, 7 do
|
||||
for x = 0, 7 do
|
||||
local r, g, b, a = id:getPixel(sx + x, sy + y)
|
||||
r, g, b, a = recolorSample(r, g, b, a, colors)
|
||||
v:setPixel((x + o) % 8, y, r, g, b, a)
|
||||
end
|
||||
end
|
||||
@@ -136,12 +150,27 @@ local function getShiftVariants(tilesetImagePath, perRow, tile)
|
||||
end
|
||||
|
||||
local frameImages = {}
|
||||
local function getFrameImages(paths)
|
||||
local key = table.concat(paths, "|")
|
||||
local function getFrameImages(paths, colors, gbcKey)
|
||||
local key = table.concat(paths, "|") .. (gbcKey or "")
|
||||
if frameImages[key] ~= nil then return frameImages[key] end
|
||||
local out = {}
|
||||
for i, path in ipairs(paths) do
|
||||
local ok, img = pcall(getImage, path)
|
||||
local ok, img = pcall(function()
|
||||
if not (colors and love.image and love.image.newImageData) then
|
||||
return getImage(path)
|
||||
end
|
||||
local id = Assets.imageData(path)
|
||||
local w, h = id:getDimensions()
|
||||
local out2 = love.image.newImageData(w, h)
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
local r, g, b, a = id:getPixel(x, y)
|
||||
r, g, b, a = recolorSample(r, g, b, a, colors)
|
||||
out2:setPixel(x, y, r, g, b, a)
|
||||
end
|
||||
end
|
||||
return love.graphics.newImage(out2)
|
||||
end)
|
||||
if not ok then
|
||||
frameImages[key] = false
|
||||
return false
|
||||
@@ -237,17 +266,33 @@ end
|
||||
-- one entry's runtime form: the tile ids it claims, the textures a step
|
||||
-- picks from, and either a step sequence (hshift/frames) or a gate
|
||||
-- (toggle). nil when the entry's pixels could not be built.
|
||||
local function buildAnim(spec, tilesetImagePath, perRow, quads)
|
||||
--
|
||||
-- gbc, when present (RED++ with a baked atlas -- see getGbcAtlas), recolors
|
||||
-- hshift/frames entries (water/flowers) the same way the atlas bakes their
|
||||
-- static tile, so they match their surroundings instead of showing raw
|
||||
-- grayscale over an otherwise fully-colored map. The "toggle" kind
|
||||
-- (spinner puzzle blur, gfx/overworld/spinners.png) is a whole-atlas clone
|
||||
-- built from the ORIGINAL grayscale atlas, not worth recoloring for a rare,
|
||||
-- gameplay-gated blur -- it is skipped under gbc, same as the buildAnim
|
||||
-- caller already does for a texture-build failure (the static, correctly-
|
||||
-- colored tile shows through unanimated).
|
||||
local function buildAnim(spec, tilesetImagePath, perRow, quads, gbc)
|
||||
local tiles = spec.tiles
|
||||
if not tiles then
|
||||
if spec.tile == nil then return nil end
|
||||
tiles = { spec.tile }
|
||||
end
|
||||
local period = spec.period or ANIM_PERIOD
|
||||
local colors
|
||||
if gbc then
|
||||
local group = PaletteFX.worldGroupAt(gbc.tilesetId, gbc.mapId, tiles[1])
|
||||
colors = group and gbc.groupColors[group + 1]
|
||||
end
|
||||
if spec.kind == "hshift" then
|
||||
local offsets = spec.offsets
|
||||
if not offsets or #offsets == 0 then return nil end
|
||||
local textures = getShiftVariants(tilesetImagePath, perRow, tiles[1])
|
||||
local textures = getShiftVariants(tilesetImagePath, perRow, tiles[1],
|
||||
colors, gbc and gbc.key)
|
||||
if not textures then return nil end
|
||||
local sequence = {}
|
||||
for i, offset in ipairs(offsets) do sequence[i] = offset + 1 end
|
||||
@@ -256,11 +301,12 @@ local function buildAnim(spec, tilesetImagePath, perRow, quads)
|
||||
elseif spec.kind == "frames" then
|
||||
local sequence = spec.sequence
|
||||
if not (spec.images and sequence and #sequence > 0) then return nil end
|
||||
local textures = getFrameImages(spec.images)
|
||||
local textures = getFrameImages(spec.images, colors, gbc and gbc.key)
|
||||
if not textures then return nil end
|
||||
return { tiles = tiles, textures = textures, sequence = sequence,
|
||||
period = period }
|
||||
elseif spec.kind == "toggle" then
|
||||
if gbc then return nil end
|
||||
local image = getToggleImage(spec, tilesetImagePath, perRow)
|
||||
if not image then return nil end
|
||||
-- the patch texture is a whole-atlas clone, so each cell needs the
|
||||
@@ -271,10 +317,82 @@ local function buildAnim(spec, tilesetImagePath, perRow, quads)
|
||||
return nil
|
||||
end
|
||||
|
||||
function TileRenderer.new(map)
|
||||
-- True GBC overworld coloring (COLORS=RED++): recolor the WHOLE tileset
|
||||
-- atlas once, per (tileset image, map), rather than trying to retrofit the
|
||||
-- SGB zone/shade-remap-shader post-process (built for a handful of coarse
|
||||
-- screen regions) into per-tile precision -- pokered-gbc's real model is
|
||||
-- "one of 8 four-color BG palettes baked per tile GRAPHIC"
|
||||
-- (color/loadpalettes.asm LoadTilesetPalette), which is exactly a
|
||||
-- recolored atlas, not a shader pass. Every existing draw path (batches,
|
||||
-- quads, border fill) then just works unmodified, with no shader at
|
||||
-- draw time; OverworldState.sgbWorldZones skips the shade-remap zone pass
|
||||
-- entirely when this is active (re-running it over already-true-color
|
||||
-- pixels would corrupt them), and SpriteRenderer's own OBP bake composites
|
||||
-- on top with ordinary alpha blending -- no trueColor exemption needed,
|
||||
-- because there is no shader left for it to be exempted from.
|
||||
--
|
||||
-- Only the ROOF group (index 6, OVERWORLD/PLATEAU only) varies by town
|
||||
-- (LoadTownPalette); Route 6's mid-map Saffron-roof y<2 split is not
|
||||
-- reproduced (it would need a rebuild on crossing the boundary for two
|
||||
-- tile-rows of one route -- not worth the complexity), so it bakes with
|
||||
-- the route's own default roof (Vermilion's) throughout.
|
||||
local gbcAtlasCache = {}
|
||||
|
||||
local function getGbcAtlas(imagePath, tilesetId, mapId, perRow, data)
|
||||
local key = imagePath .. "#gbc:" .. mapId
|
||||
if gbcAtlasCache[key] ~= nil then return gbcAtlasCache[key] or nil end
|
||||
local img = false
|
||||
if love.image and love.image.newImageData then
|
||||
local groupColors = PaletteFX.worldGroupColors(data, tilesetId, mapId, nil)
|
||||
if groupColors then
|
||||
local src = Assets.imageData(imagePath)
|
||||
local iw, ih = src:getDimensions()
|
||||
local out = love.image.newImageData(iw, ih)
|
||||
local tileColors = {}
|
||||
for t = 0, (iw / 8) * (ih / 8) - 1 do
|
||||
local colors = tileColors[t]
|
||||
if colors == nil then
|
||||
local group = PaletteFX.worldGroupAt(tilesetId, mapId, t)
|
||||
colors = (group and groupColors[group + 1]) or false
|
||||
tileColors[t] = colors
|
||||
end
|
||||
local ox, oy = (t % perRow) * 8, math.floor(t / perRow) * 8
|
||||
for py = 0, 7 do
|
||||
for px = 0, 7 do
|
||||
local sx, sy = ox + px, oy + py
|
||||
local r, g, b, a = src:getPixel(sx, sy)
|
||||
r, g, b, a = recolorSample(r, g, b, a, colors)
|
||||
out:setPixel(sx, sy, r, g, b, a)
|
||||
end
|
||||
end
|
||||
end
|
||||
img = love.graphics.newImage(out)
|
||||
end
|
||||
end
|
||||
gbcAtlasCache[key] = img
|
||||
return img or nil
|
||||
end
|
||||
|
||||
-- data: Game.data (threaded through explicitly, not required lazily, so
|
||||
-- headless tests that build a map from a plain local table still work)
|
||||
function TileRenderer.new(map, data)
|
||||
local self = setmetatable({}, TileRenderer)
|
||||
self.map = map
|
||||
self.data = data
|
||||
self.image = getImage(map.tileset.image)
|
||||
local gbcCtx
|
||||
if data and PaletteFX.usesGbcPack() and PaletteFX.hasWorldTileset(map.tileset.id) then
|
||||
local gbc = getGbcAtlas(map.tileset.image, map.tileset.id, map.id,
|
||||
map.tileset.tilesPerRow, data)
|
||||
if gbc then
|
||||
self.image = gbc
|
||||
self.gbcAtlas = true
|
||||
-- also recolors the animated water/flower entries below, so they
|
||||
-- match the atlas's static tiles instead of showing raw grayscale
|
||||
gbcCtx = { tilesetId = map.tileset.id, mapId = map.id, key = "#gbc:" .. map.id,
|
||||
groupColors = PaletteFX.worldGroupColors(data, map.tileset.id, map.id, nil) }
|
||||
end
|
||||
end
|
||||
-- a full-color atlas colors everything it paints, ring and border fill
|
||||
-- included, so every draw entry point claims its rect out of the pass
|
||||
self.trueColor = map.tileset.trueColor or nil
|
||||
@@ -301,7 +419,7 @@ function TileRenderer.new(map)
|
||||
local declared = map.tileset.animatedTiles
|
||||
or TileRenderer.defaultAnimatedTiles(map.tileset)
|
||||
for _, spec in ipairs(declared) do
|
||||
local anim = buildAnim(spec, map.tileset.image, perRow, self.quads)
|
||||
local anim = buildAnim(spec, map.tileset.image, perRow, self.quads, gbcCtx)
|
||||
if anim then
|
||||
anim.cells = {}
|
||||
anims[#anims + 1] = anim
|
||||
@@ -487,7 +605,10 @@ end
|
||||
|
||||
-- rebuild after a block change (Cut trees)
|
||||
function TileRenderer:rebuild()
|
||||
local fresh = TileRenderer.new(self.map)
|
||||
local fresh = TileRenderer.new(self.map, self.data)
|
||||
self.image = fresh.image
|
||||
self.gbcAtlas = fresh.gbcAtlas
|
||||
self.quads = fresh.quads
|
||||
self.ringBatch = fresh.ringBatch
|
||||
self.mapBatch = fresh.mapBatch
|
||||
self.anims = fresh.anims
|
||||
|
||||
Reference in New Issue
Block a user