Files
gen1recomp/src/ui/PaletteScreen.lua
T
2026-08-26 12:04:06 -04:00

579 lines
15 KiB
Lua

local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local Palette = require("src.render.Palette")
local PaletteFX = require("src.render.PaletteFX")
local PaletteScreen = {}
PaletteScreen.__index = PaletteScreen
PaletteScreen.isOpaque = true
local COLS, ROWS = 5, 4
local TILE_W, TILE_H = 28, 20
local GAP = 4
local GRID_X, GRID_Y = 2, 14
local PER_PAGE = COLS * ROWS
local SCREEN_W, SCREEN_H = 160, 144
local BACK = 0
local function sanitize(text)
text = tostring(text or ""):upper():gsub("_", "'")
text = text:gsub("[^A-Z0-9 %.'%-]", " ")
text = text:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
return text
end
PaletteScreen.sanitize = sanitize -- named for the suite
local function wrap(text, width, lines)
local out = {}
local rest = text
for i = 1, lines do
if rest == "" then break end
if #rest <= width then
out[i] = rest
rest = ""
else
local cut = rest:sub(1, width + 1):match("^.*() ")
if not cut or cut < 2 then cut = width + 1 end
out[i] = rest:sub(1, cut - 1)
rest = rest:sub(cut):gsub("^ +", "")
end
end
if rest ~= "" and #out > 0 then
local last = out[#out]
out[#out] = last:sub(1, math.max(0, width - 1)) .. "."
end
return out
end
PaletteScreen.wrap = wrap -- named for the suite
local function packLabel(name, width)
local text = sanitize(name)
if #text > width then
local leaf = tostring(name):match("([^/]+)$")
if leaf then text = sanitize(leaf) end
end
return wrap(text, width, 1)[1] or ""
end
PaletteScreen.packLabel = packLabel -- named for the suite
local function fill(x, y, w, h, c)
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, 1)
love.graphics.rectangle("fill", x, y, w, h)
end
local function frame(x, y, w, h, shade)
love.graphics.setColor(shade, shade, shade, 1)
love.graphics.rectangle("line", x + 0.5, y + 0.5, w - 1, h - 1)
end
local function drawSwatch(x, y, w, h, swatch)
if not swatch then
frame(x, y, w, h, 0.5)
return
end
local band = w / 4
for i = 1, 4 do
fill(x + math.floor((i - 1) * band), y,
math.ceil(band), h, swatch[i])
end
end
PaletteScreen.drawSwatch = drawSwatch -- named for the suite
local function drawBack(selected)
if selected then
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", 0, 0, 10, 9)
love.graphics.setColor(1, 1, 1, 1)
else
love.graphics.setColor(0, 0, 0, 1)
end
for i = 0, 3 do
love.graphics.rectangle("fill", 2 + i, 4 - i, 1, 1 + i * 2)
end
end
PaletteScreen.drawBack = drawBack -- named for the suite
local function engineModes()
local labels = PaletteFX.MODE_LABELS or {}
local out = {}
for i, id in ipairs(PaletteFX.MODES) do
out[i] = { PaletteFX.modeLabel(id) or labels[id] or id:upper(), id }
end
return out
end
local function dropConflictingMode(game)
local mode = PaletteFX.mode
local bakes = PaletteFX.usesGbcPack(mode) or PaletteFX.usesSpriteObp(mode)
if not bakes then return end
local o = game.save.options
o.colors = "gbc"
PaletteFX.setMode("gbc")
end
local function liveOpts(game)
dropConflictingMode(game)
return {
palette = Palette,
get = function() return game.save.options.palette or "" end,
set = function(v)
local o = game.save.options
o.palette = v
if v ~= "" then dropConflictingMode(game) end
PaletteFX.setCustomRamp(v ~= "" and Palette.ramp(v) or nil)
if game.writeOptions then pcall(game.writeOptions, game) end
end,
modes = engineModes(),
getMode = function() return game.save.options.colors or "gbc" end,
setMode = function(v)
local o = game.save.options
o.colors = v
PaletteFX.setMode(v)
if game.writeOptions then pcall(game.writeOptions, game) end
end,
}
end
function PaletteScreen.new(game, opts)
opts = opts or liveOpts(game)
local self = setmetatable({
game = game,
Palette = opts.palette,
get = opts.get, set = opts.set,
modes = opts.modes or {},
getMode = opts.getMode, setMode = opts.setMode,
onChange = opts.onChange,
view = "root",
index = 1,
scroll = 0,
group = nil,
stackTrail = {},
}, PaletteScreen)
self.rows = self:buildRoot()
self:enterFromSelection()
return self
end
function PaletteScreen:buildRoot()
local rows = {}
for _, category in ipairs(self.Palette and self.Palette.categories() or {}) do
if #(category.palettes or {}) > 0 then
rows[#rows + 1] = { label = category.name, folder = true,
view = "tiles", group = category }
end
end
if self.Palette and self.Palette.MONO_COLOURS then
rows[#rows + 1] = { label = "MONOCHROME", folder = true, view = "mono" }
end
for _, mode in ipairs(self.modes) do
rows[#rows + 1] = { label = mode[1], mode = mode[2] }
end
return rows
end
function PaletteScreen:rootIndexFor(view, group)
for i, row in ipairs(self.rows or {}) do
if row.view == view then
if view ~= "tiles" then return i end
if row.group and group and row.group.key == group.key then return i end
end
end
return 1
end
function PaletteScreen:enterFromSelection()
local Palette = self.Palette
if not Palette then return end
local id = self.get and self.get() or ""
if type(id) ~= "string" or id == "" then return end
if id:match("^m:") then
for i, c in ipairs(Palette.MONO_COLOURS) do
if Palette.monoId(c[2], c[3], c[4]) == id then
self:openAt("mono", nil, i)
return
end
end
return
end
local key, at = Palette.locate(id)
if key then self:openAt("tiles", Palette.category(key), at) end
end
function PaletteScreen:openAt(view, group, index)
self.stackTrail = { { view = "root", index = self:rootIndexFor(view, group),
scroll = 0, group = nil } }
self.view, self.group, self.index, self.scroll = view, group, index or 1, 0
local items = self:items()
self:clampScroll(#items, #items + 1, PER_PAGE)
end
function PaletteScreen:items()
local Palette = self.Palette
if self.view == "root" then return self.rows or {} end
if self.view == "tiles" then
return self.group and self.group.palettes or {}
end
if self.view == "mono" then
return Palette and Palette.MONO_COLOURS or {}
end
return {}
end
function PaletteScreen:isGrid()
return self.view == "tiles" or self.view == "mono"
end
function PaletteScreen:idAt(i)
local Palette = self.Palette
if not Palette then return nil end
if self.view == "tiles" then
local entry = self.group and (self.group.palettes or {})[i]
return entry and entry.id or nil
elseif self.view == "mono" then
local c = Palette.MONO_COLOURS[i]
return c and Palette.monoId(c[2], c[3], c[4]) or nil
end
return nil
end
function PaletteScreen:title()
if self.view == "root" then return Strings("COLOUR") end
if self.view == "mono" then return Strings("MONOCHROME") end
return sanitize(self.group and self.group.name or "")
end
function PaletteScreen:update()
local input = self.game.input
local items = self:items()
local total = #items
local cancel = total + 1
if input:wasPressed("b") then
self:back()
return
end
if self:isGrid() then
self:moveGrid(input, total, cancel)
else
self:moveList(input, total, cancel)
end
if input:wasPressed("a") then
self:activate(cancel)
end
end
function PaletteScreen:moveList(input, total, cancel)
if self.index == BACK then
if input:wasPressed("down") or input:wasPressed("right") then
self.index = 1
end
return
end
if input:wasPressed("left") then
self.index = BACK
return
end
if input:wasPressed("up") then
self.index = self.index > 1 and self.index - 1 or BACK
elseif input:wasPressed("down") then
self.index = self.index < cancel and self.index + 1 or 1
end
self:clampScroll(total, cancel, 12)
end
function PaletteScreen:moveGrid(input, total, cancel)
local i = self.index
if i == BACK then
if input:wasPressed("down") or input:wasPressed("right") then
self.index = self.scroll + 1
if self.index > total then self.index = 1 end
end
return
end
if i > total then
if input:wasPressed("up") then
self.index = total
elseif input:wasPressed("down") then
self.index = 1
end
self:clampScroll(total, cancel, PER_PAGE)
return
end
if input:wasPressed("left") then
self.index = ((i - 1) % COLS == 0) and BACK or i - 1
elseif input:wasPressed("right") then
self.index = i < total and i + 1 or 1
elseif input:wasPressed("up") then
self.index = (i - COLS >= 1) and i - COLS or BACK
elseif input:wasPressed("down") then
if i + COLS <= total then
self.index = i + COLS
else
self.index = cancel
end
end
self:clampScroll(total, cancel, PER_PAGE)
end
function PaletteScreen:clampScroll(total, cancel, visible)
if self.index == BACK then return end
if self.index >= cancel then
self.scroll = math.max(0, total - visible)
if self:isGrid() then
self.scroll = math.floor(self.scroll / COLS) * COLS
end
return
end
local step = self:isGrid() and COLS or 1
if self.index <= self.scroll then
self.scroll = math.floor((self.index - 1) / step) * step
elseif self.index > self.scroll + visible then
self.scroll = math.floor((self.index - 1) / step) * step - visible + step
end
if self.scroll < 0 then self.scroll = 0 end
end
function PaletteScreen:activate(cancel)
if self.index == BACK or self.index >= cancel then
self:back()
return
end
if self.view == "root" then
local row = (self.rows or {})[self.index]
if not row then return end
if row.folder then
self:push(row.view, row.group)
elseif row.mode then
if self.set then self.set("") end
if self.setMode then self.setMode(row.mode) end
self:changed()
end
elseif self:isGrid() then
local id = self:idAt(self.index)
if id then
if self.set then self.set(id) end
self:changed()
end
end
end
function PaletteScreen:changed()
if self.game.data and Sound and Sound.play then
pcall(Sound.play, self.game.data, "Press_AB")
end
if self.onChange then self.onChange() end
end
function PaletteScreen:push(view, group)
self.stackTrail[#self.stackTrail + 1] =
{ view = self.view, index = self.index, scroll = self.scroll,
group = self.group }
self.view, self.index, self.scroll = view, 1, 0
self.group = group
self:cursorToSelection()
end
function PaletteScreen:cursorToSelection()
local id = self.get and self.get() or ""
if type(id) ~= "string" or id == "" then return end
local items = self:items()
for i = 1, #items do
if self:idAt(i) == id then
self.index = i
self:clampScroll(#items, #items + 1,
self:isGrid() and PER_PAGE or 12)
return
end
end
end
function PaletteScreen:back()
local prev = table.remove(self.stackTrail)
if prev then
self.view, self.index = prev.view, prev.index
self.scroll, self.group = prev.scroll, prev.group
return
end
self:close()
end
function PaletteScreen:close()
if self.game.data and Sound and Sound.play then
pcall(Sound.play, self.game.data, "Press_AB")
end
self.game.stack:pop()
end
function PaletteScreen:sgbPalettes()
return nil
end
function PaletteScreen:draw()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
drawBack(self.index == BACK)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(self:title(), 12, 0)
if self:isGrid() then
self:drawGrid()
else
self:drawList()
end
love.graphics.setColor(0, 0, 0, 1)
local items = self:items()
if self.index >= #items + 1 then
Font.draw(Strings("CANCEL"), 16, SCREEN_H - 8)
Font.drawCode(Theme.cursor, 8, SCREEN_H - 8)
end
love.graphics.setColor(1, 1, 1, 1)
end
function PaletteScreen:drawList()
local items = self:items()
local mode = self.getMode and self.getMode() or nil
local visible = 12
for slot = 1, visible do
local i = self.scroll + slot
local row = items[i]
if not row then break end
local y = 16 + (slot - 1) * 8
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings(row.label or ""), 16, y)
if i == self.index then
Font.drawCode(Theme.cursor, 8, y)
end
if row.folder then
Font.drawCode(Theme.cursor, 8 + 8 * 17, y)
elseif row.mode and mode and row.mode == mode then
love.graphics.rectangle("fill", 8 + 8 * 17 + 2, y + 2, 4, 4)
end
end
end
function PaletteScreen:drawGrid()
local Palette = self.Palette
local items = self:items()
local total = #items
for slot = 1, PER_PAGE do
local i = self.scroll + slot
if i > total then break end
local col = (slot - 1) % COLS
local row = math.floor((slot - 1) / COLS)
local x = GRID_X + col * (TILE_W + GAP)
local y = GRID_Y + row * (TILE_H + GAP)
local swatch
if self.view == "mono" then
local c = Palette and Palette.MONO_COLOURS[i]
if c then
local ramp = Palette.monoRamp(c[2], c[3], c[4])
swatch = { ramp[4], ramp[3], ramp[2], ramp[1] }
end
else
swatch = Palette and Palette.swatch(self:idAt(i)) or nil
end
drawSwatch(x, y, TILE_W, TILE_H, swatch)
if i == self.index then
frame(x - 2, y - 2, TILE_W + 4, TILE_H + 4, 0)
frame(x - 1, y - 1, TILE_W + 2, TILE_H + 2, 1)
end
end
self:drawFooter(total)
end
function PaletteScreen:drawFooter(total)
local Palette = self.Palette
love.graphics.setColor(0, 0, 0, 1)
local name, pack = "", nil
if self.index >= 1 and self.index <= total then
if self.view == "mono" then
local c = Palette and Palette.MONO_COLOURS[self.index]
name = c and c[1] or ""
else
local entry = self.group and (self.group.palettes or {})[self.index]
if entry then
name = sanitize(entry.name)
pack = entry.pack
end
end
end
local lines = wrap(name, 20, 2)
for i = 1, #lines do
Font.draw(lines[i], 0, 110 + (i - 1) * 8)
end
local at = 0
if total > PER_PAGE then
local page = math.floor(self.scroll / PER_PAGE) + 1
local pages = math.ceil(total / PER_PAGE)
Font.draw(("%d/%d"):format(page, pages), 0, 128)
at = 40
end
if pack then
Font.draw(packLabel(pack, (SCREEN_W - at) / 8), at, 128)
end
local stored = self.get and self.get() or ""
if type(stored) == "string" and stored ~= "" then
for slot = 1, PER_PAGE do
local i = self.scroll + slot
if i > total then break end
if self:idAt(i) == stored then
local col = (slot - 1) % COLS
local row = math.floor((slot - 1) / COLS)
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle(
"fill",
GRID_X + col * (TILE_W + GAP) + TILE_W - 5,
GRID_Y + row * (TILE_H + GAP) + TILE_H - 5, 4, 4)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle(
"fill",
GRID_X + col * (TILE_W + GAP) + TILE_W - 4,
GRID_Y + row * (TILE_H + GAP) + TILE_H - 4, 2, 2)
break
end
end
end
end
return PaletteScreen