678 lines
25 KiB
Text
678 lines
25 KiB
Text
--[[
|
|
*
|
|
* Singe 3
|
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*
|
|
*
|
|
--]]
|
|
|
|
|
|
|
|
-- The menu drawn with sprites and fontPrint straight into the overlay: the look Singe 2 had, and
|
|
-- the one that works on a machine whose GPU device never came up, where there is no GUI document to
|
|
-- build. Singe/MenuDocument.singe is the other renderer; Singe/Menu.singe loads exactly one of the
|
|
-- two and calls it through MENU_RENDER. See the top of MenuDocument.singe for what is in it.
|
|
--
|
|
-- One game is on screen at a time, as it always was here, so there is no list to page through
|
|
-- visually: up and down move one game, left and right move a page, and the line above the details
|
|
-- says where in the library the selection is. An entry with more to say than fits walks down by
|
|
-- itself a line at a time, rests, and walks back up again: up and down belong to the library, and
|
|
-- a cabinet has no key to spare for scrolling a paragraph. Page up and page down override it for
|
|
-- whoever does have a keyboard.
|
|
|
|
|
|
local MARGIN = 10 -- Around the page, and between the regions on it.
|
|
local GAP = 8
|
|
local HINT_H = 24 -- The line of key hints along the bottom.
|
|
local SIDE_FRAC = 0.40 -- How much of the width the marquee and the attract video take.
|
|
local MARQUEE_ASPECT_W = 320 -- What the artwork and the clips are authored at; both regions keep
|
|
local MARQUEE_ASPECT_H = 75 -- these proportions whatever the overlay's size turns out to be.
|
|
local VIDEO_ASPECT_W = 320
|
|
local VIDEO_ASPECT_H = 200
|
|
local LINE_HEIGHT = 24
|
|
local LABEL_W = 150
|
|
local SUB_INDENT = 20
|
|
local CARD_PAD = 14 -- The card the tools draw on: inside its border to its text,
|
|
local TITLE_H = 36 -- its title bar, and its footer of key hints, one line each plus
|
|
local KEY_LINE = 18 -- its padding. The colours are the game page's: the panel, the
|
|
local KEY_PAD = 6 -- selection's gradient, the blue-grey border, the gold, the quiet
|
|
local BORDER = 2 -- blue-grey of a label.
|
|
local ITEM_INSET = 8
|
|
local CLEAR_FRAC = 0.70 -- How wide a strip a page that lets the overlay through may take.
|
|
local NOTE_FRAC = 0.42 -- How much of the card a note beside the list takes.
|
|
local PANEL = { 24, 38, 64, 216 }
|
|
local PANEL_DEEP = { 12, 20, 36, 216 }
|
|
local FOOT = { 16, 24, 40, 208 }
|
|
local TITLE_BAR = { 51, 80, 122, 255 }
|
|
local TITLE_BAR_L = { 74, 110, 168, 255 }
|
|
local EDGE = { 106, 134, 184, 255 }
|
|
local GOLD = { 255, 211, 90, 255 }
|
|
local INK = { 240, 240, 240, 255 }
|
|
local QUIET = { 143, 162, 196, 255 }
|
|
local SOFT = { 200, 210, 230, 255 }
|
|
local PAGE_ROWS = 10
|
|
local WRAP_BREAK = " [!wb!] "
|
|
local HINTS = "Up / Down: select Left / Right: page PgUp / PgDn: details Start or button: play Service: tools"
|
|
|
|
local font = nil
|
|
local hintFont = nil -- Smaller: the hints are a footnote, and at the body's size the line
|
|
-- of them ran off the end of a 720 pixel overlay.
|
|
local titleFont = nil -- Larger: a tool's title on its bar.
|
|
local introDone = false
|
|
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
|
local marqueeX, marqueeY, marqueeW, marqueeH = 0, 0, 0, 0
|
|
local textX, textY, textW, textH = 0, 0, 0, 0
|
|
local hintsX, hintsY = 0, 0
|
|
local cabinetX, cabinetY, cabinetW, cabinetH = 0, 0, 0, 0
|
|
local spriteCabinet = nil
|
|
local spriteMarquee = nil
|
|
local textSprites = {}
|
|
local textShadows = {}
|
|
local textFirst = 0 -- The first line of the details on show, which walks by itself.
|
|
local textDown = true -- and which way it is walking.
|
|
local textClock = 0
|
|
local SHADOW = 2 -- How far the shadow sits down and across from the text.
|
|
local SCROLL_HOLD = 3.0 -- Seconds the details rest at each end before moving.
|
|
local SCROLL_STEP = 1.1 -- and seconds a line while they are moving.
|
|
local textHeight = 0
|
|
local textLimit = 0
|
|
|
|
|
|
-- A games.dat entry need not carry every field, and one that is missing must not take the menu
|
|
-- down; the document renderer is tolerant the same way, through menuEscape.
|
|
local function field(value)
|
|
return tostring(value or "")
|
|
end
|
|
|
|
|
|
-- Shrinks a sprite to fit inside a region, keeping its proportions. spriteDraw blits at the
|
|
-- sprite's own size and a games.dat's artwork is whatever size the person who drew it chose, so
|
|
-- without this a big cabinet picture is drawn straight over whatever the page put beside it. Only
|
|
-- ever smaller: blowing a small picture up to fill the space makes it soft for nothing.
|
|
local function fitSprite(handle, boxW, boxH)
|
|
local width = spriteGetWidth(handle)
|
|
local height = spriteGetHeight(handle)
|
|
|
|
if (width > 0) and (height > 0) then
|
|
local scale = math.min(boxW / width, boxH / height)
|
|
|
|
if scale < 1.0 then
|
|
spriteScale(handle, scale)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Splits text into the lines that fit the width given, measuring with the selected font. The only
|
|
-- way to measure a string is to render it, so this is slow enough to be worth doing once when
|
|
-- something changes rather than once a frame. WRAP_BREAK is a word the caller puts in to force a
|
|
-- line ending.
|
|
--
|
|
-- The space between words is kept as it was written rather than squeezed to one, because a tool
|
|
-- lines its columns up with runs of spaces and there is no other way here to say "a wide gap".
|
|
local function fitLines(text, maxWidth)
|
|
local lines = {}
|
|
local line = ""
|
|
local pending = ""
|
|
local trimBreak = utilTrim(WRAP_BREAK)
|
|
|
|
for word, space in (text .. WRAP_BREAK):gmatch("(%S+)(%s*)") do
|
|
if word == trimBreak then
|
|
lines[#lines + 1] = line
|
|
line = ""
|
|
pending = ""
|
|
else
|
|
local candidate = line .. pending .. word
|
|
local measured = fontToSprite(candidate)
|
|
local tooWide = spriteGetWidth(measured) > maxWidth
|
|
spriteUnload(measured)
|
|
if tooWide and line ~= "" then
|
|
lines[#lines + 1] = line
|
|
line = word
|
|
else
|
|
line = candidate
|
|
end
|
|
pending = space
|
|
end
|
|
end
|
|
if line ~= "" then
|
|
lines[#lines + 1] = line
|
|
end
|
|
|
|
return lines
|
|
end
|
|
|
|
|
|
-- The service tools, laid out. Each entry is one line of the page and each line is a list of
|
|
-- pieces: where the piece starts, what it says and which colour it wants. The colours are roles
|
|
-- rather than values because the audio delay tool inverts the whole page for a frame, and fontPrint
|
|
-- takes the colour at the moment it draws -- unlike fontToSprite, which bakes it in.
|
|
local toolLines = {}
|
|
local bodyX, bodyW = 0, 0 -- The card's body, where wrapped text is measured against.
|
|
|
|
|
|
local function toolLine(pieces)
|
|
toolLines[#toolLines + 1] = pieces
|
|
end
|
|
|
|
|
|
-- Text too long for the page wraps under itself rather than running off the edge.
|
|
local function toolWrapped(text, x, role)
|
|
if text == nil or text == "" then
|
|
-- Still a line: a tool that says nothing this frame should not have its page jump about.
|
|
toolLine({})
|
|
return
|
|
end
|
|
for _, line in ipairs(fitLines(text, bodyX + bodyW - x)) do
|
|
toolLine({ { x = x, text = line, role = role } })
|
|
end
|
|
end
|
|
|
|
|
|
-- The card the tools draw on: its edges, and where its body and its columns begin. A page that
|
|
-- lets the overlay through has no card; its strip is measured once its lines are known.
|
|
local toolKeyLines = {} -- The key hints, for the card's footer.
|
|
local toolNoteLines = {} -- What the chosen tool is for, beside the list of tools.
|
|
local cardX, cardY, cardW, cardH = 0, 0, 0, 0
|
|
local noteX, noteW = 0, 0
|
|
|
|
|
|
local function toolNote(pieces)
|
|
toolNoteLines[#toolNoteLines + 1] = pieces
|
|
end
|
|
|
|
|
|
local function toolsLayout()
|
|
local width = overlayGetWidth()
|
|
local height = overlayGetHeight()
|
|
-- Only the rows that fit, with the chosen one kept in view; a catalogue is as long as the
|
|
-- service is big and would otherwise run off the bottom of the screen.
|
|
local rows, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
|
|
|
cardX, cardY, cardW, cardH = MARGIN, MARGIN, width - MARGIN * 2, height - MARGIN * 2
|
|
if TOOL_CLEAR then
|
|
cardW = math.floor(width * CLEAR_FRAC)
|
|
cardX = math.floor((width - cardW) / 2)
|
|
end
|
|
bodyX = cardX + BORDER + CARD_PAD
|
|
bodyW = cardW - (BORDER + CARD_PAD) * 2
|
|
local fullW = bodyW
|
|
-- A page with a note has a second column for it beside the list.
|
|
noteX, noteW = nil, 0
|
|
for _, row in ipairs(rows) do
|
|
if row.kind == "note" then
|
|
noteW = math.floor(cardW * NOTE_FRAC) - (BORDER + CARD_PAD) * 2
|
|
noteX = cardX + cardW - BORDER - CARD_PAD - noteW
|
|
bodyW = noteX - CARD_PAD - BORDER - bodyX
|
|
end
|
|
end
|
|
local valueX = bodyX + LABEL_W
|
|
|
|
toolLines = {}
|
|
toolKeyLines = {}
|
|
toolNoteLines = {}
|
|
if more then
|
|
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
|
end
|
|
local seenItem = false
|
|
for _, row in ipairs(rows) do
|
|
-- The "more below" marker belongs immediately under the last item, not at the very bottom
|
|
-- of the page under the key hints, which are not part of the list.
|
|
if below and seenItem and row.kind ~= "item" then
|
|
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
|
below = false
|
|
end
|
|
if row.kind == "item" then
|
|
seenItem = true
|
|
end
|
|
if row.kind == "blank" then
|
|
toolLine({})
|
|
elseif row.kind == "pair" then
|
|
-- The label and the first line of the value share a line; the rest of the value wraps
|
|
-- under itself, in its own column.
|
|
local lines = fitLines(row.value, bodyX + bodyW - valueX)
|
|
toolLine({ { x = bodyX, text = row.label, role = "quiet" },
|
|
{ x = valueX, text = lines[1] or "", role = "ink" } })
|
|
for index = 2, #lines do
|
|
toolLine({ { x = valueX, text = lines[index], role = "ink" } })
|
|
end
|
|
elseif row.kind == "sub" then
|
|
for _, line in ipairs(fitLines(row.text, bodyW - SUB_INDENT)) do
|
|
toolLine({ { x = bodyX + SUB_INDENT, text = line, role = "soft" } })
|
|
end
|
|
elseif row.kind == "keys" then
|
|
-- The footer, in the smaller font, across the whole card.
|
|
fontSelect(hintFont)
|
|
for _, line in ipairs(fitLines(row.text, fullW)) do
|
|
toolKeyLines[#toolKeyLines + 1] = line
|
|
end
|
|
fontSelect(font)
|
|
elseif row.kind == "item" then
|
|
local pieces = { { x = bodyX + ITEM_INSET, text = row.text, role = row.selected and "pick" or "ink", bar = row.selected } }
|
|
|
|
-- The detail against the right edge of the row, so a column of them lines up. An
|
|
-- empty one -- a field not filled in yet -- is nothing to measure or draw.
|
|
local detail = row.detail and tostring(row.detail) or ""
|
|
|
|
if detail ~= "" then
|
|
local measured = fontToSprite(detail)
|
|
|
|
pieces[#pieces + 1] = { x = bodyX + bodyW - ITEM_INSET - spriteGetWidth(measured), text = detail, role = pieces[1].role }
|
|
spriteUnload(measured)
|
|
end
|
|
toolLine(pieces)
|
|
elseif row.kind == "note" then
|
|
for _, line in ipairs(fitLines(row.text, noteW)) do
|
|
toolNote({ { x = noteX, text = line, role = "soft" } })
|
|
end
|
|
else
|
|
toolWrapped(row.text, bodyX, "ink")
|
|
end
|
|
end
|
|
-- A list that runs to the very end of the page has no row after it to hang the marker on.
|
|
if below then
|
|
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
|
end
|
|
-- A strip along the bottom is as tall as its lines.
|
|
if TOOL_CLEAR then
|
|
cardH = BORDER * 2 + TITLE_H + CARD_PAD + #toolLines * LINE_HEIGHT + CARD_PAD + KEY_PAD * 2 + #toolKeyLines * KEY_LINE
|
|
cardY = height - MARGIN - cardH
|
|
end
|
|
end
|
|
|
|
|
|
MENU_RENDER = {}
|
|
|
|
|
|
MENU_RENDER.begin = function()
|
|
font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
|
hintFont = fontLoad("Singe/FreeSansBold.ttf", 13)
|
|
titleFont = fontLoad("Singe/FreeSansBold.ttf", 22)
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
fontSelect(font)
|
|
end
|
|
|
|
|
|
MENU_RENDER.finish = function()
|
|
for _, handle in ipairs({ font, hintFont, titleFont }) do
|
|
if handle then
|
|
fontUnload(handle)
|
|
end
|
|
end
|
|
font, hintFont, titleFont = nil, nil, nil
|
|
end
|
|
|
|
|
|
-- The backdrop is the disc: the engine intro, then the grid section played over and over. A
|
|
-- machine with no GPU device has nothing to draw a grid with, so this renderer plays the one in
|
|
-- Singe/menuBackground.mkv; Singe/MenuDocument.singe draws its own instead.
|
|
MENU_RENDER.backdropBegin = function(showIntro)
|
|
introDone = not showIntro
|
|
discPlay()
|
|
if not showIntro then
|
|
discSkipToFrame(DISC_GRID_START)
|
|
end
|
|
end
|
|
|
|
|
|
MENU_RENDER.backdrop = function()
|
|
-- Loop the grid. A machine too slow to show every disc frame can step over the last one, after
|
|
-- which the engine starts the disc again from the top; once the intro has played, that is a loop.
|
|
if discGetFrame() >= DISC_LAST_FRAME or (introDone and discGetFrame() < DISC_GRID_START) then
|
|
discSkipToFrame(DISC_GRID_START)
|
|
end
|
|
if discGetFrame() >= DISC_GRID_START then
|
|
introDone = true
|
|
end
|
|
|
|
-- Transparent, so the disc behind the overlay is what shows.
|
|
colorBackground(0, 0, 0, 0)
|
|
overlayClear()
|
|
end
|
|
|
|
|
|
MENU_RENDER.backdropReady = function()
|
|
-- The menu comes up where the drawn backdrop's logo leaves, which is before the picture starts
|
|
-- repeating: the frames between the two are the grid running on while the music fades out.
|
|
return discGetFrame() >= DISC_MENU_FRAME
|
|
end
|
|
|
|
|
|
-- The cabinet fills the left; the marquee, the attract video and the details stack down the right.
|
|
-- The page: the cabinet on the left, the marquee and the attract video down the right, the game's
|
|
-- details across the bottom and the key hints under them. The details used to have the video's
|
|
-- column and whatever height was left over, which at 720x480 was four lines -- the description and
|
|
-- about one field -- so most of what a games.dat says never reached the screen at all. They get
|
|
-- the full width now, and the fields are grouped the way the document renderer groups them, so the
|
|
-- whole entry fits. Everything is worked out from the overlay's size rather than written down,
|
|
-- because a game may set any size it likes.
|
|
MENU_RENDER.layout = function()
|
|
local width = overlayGetWidth()
|
|
local height = overlayGetHeight()
|
|
local sideW = math.floor(width * SIDE_FRAC)
|
|
local sideX = width - MARGIN - sideW
|
|
|
|
marqueeW = sideW
|
|
marqueeH = math.floor(sideW * MARQUEE_ASPECT_H / MARQUEE_ASPECT_W)
|
|
marqueeX = sideX
|
|
marqueeY = MARGIN
|
|
|
|
videoW = sideW
|
|
videoH = math.floor(sideW * VIDEO_ASPECT_H / VIDEO_ASPECT_W)
|
|
videoX = sideX
|
|
videoY = marqueeY + marqueeH + GAP
|
|
|
|
hintsX = MARGIN
|
|
hintsY = height - MARGIN - HINT_H
|
|
|
|
textX = MARGIN
|
|
textY = videoY + videoH + GAP
|
|
textW = width - MARGIN - textX
|
|
textH = hintsY - GAP - textY
|
|
|
|
cabinetX = MARGIN
|
|
cabinetY = MARGIN
|
|
cabinetW = sideX - GAP - cabinetX
|
|
cabinetH = textY - GAP - cabinetY
|
|
end
|
|
|
|
|
|
MENU_RENDER.buildList = function()
|
|
-- One game is on screen at a time; there is no list to build.
|
|
end
|
|
|
|
|
|
MENU_RENDER.showGame = function(game)
|
|
spriteCabinet = spriteLoad(menuArtPath(game.CABINET))
|
|
spriteMarquee = spriteLoad(menuArtPath(game.MARQUEE))
|
|
fitSprite(spriteCabinet, cabinetW, cabinetH)
|
|
fitSprite(spriteMarquee, marqueeW, marqueeH)
|
|
|
|
-- The details, grouped the way the document renderer groups them: the short fields share a
|
|
-- line, and the description comes last because it is the one that can run on. It used to come
|
|
-- first, which meant a long description pushed every fact about the game off the bottom.
|
|
local textBox = "Year: " .. field(game.YEAR) .. " Genre: " .. field(game.GENRE) ..
|
|
" Platform: " .. field(game.PLATFORM) .. WRAP_BREAK ..
|
|
"Developer: " .. field(game.DEVELOPER) .. WRAP_BREAK ..
|
|
"Publisher: " .. field(game.PUBLISHER) .. WRAP_BREAK ..
|
|
"Singe port: " .. field(game.CREATOR) .. " Source: " .. field(game.SOURCE) .. WRAP_BREAK ..
|
|
WRAP_BREAK .. field(game.DESCRIPTION)
|
|
-- fontToSprite bakes the colour in, so it is set before the sprites are made rather than
|
|
-- before they are drawn.
|
|
-- Twice over: black first, drawn a couple of pixels down and across behind the white, because
|
|
-- this page sits on a moving grid and white on magenta is hard to read. The document renderer
|
|
-- puts a translucent panel behind its text; nothing here can fill a rectangle, and a shadow
|
|
-- costs one more blit a line rather than one more surface a frame.
|
|
local lines = fitLines(textBox, textW)
|
|
|
|
colorForeground(0, 0, 0, 255)
|
|
for _, line in ipairs(lines) do
|
|
textShadows[#textShadows + 1] = (string.len(line) > 0) and fontToSprite(line) or -1
|
|
end
|
|
colorForeground(255, 255, 255, 255)
|
|
for _, line in ipairs(lines) do
|
|
textSprites[#textSprites + 1] = (string.len(line) > 0) and fontToSprite(line) or -1
|
|
end
|
|
|
|
-- How tall a line is, and how many of them fit under the title. Measured here rather than
|
|
-- part way through drawing, because the scroll has to know before the first line is drawn.
|
|
textHeight = 0
|
|
for _, handle in ipairs(textSprites) do
|
|
if (textHeight == 0) and (handle >= 0) then
|
|
textHeight = spriteGetHeight(handle) + 1
|
|
end
|
|
end
|
|
textLimit = (textHeight > 0) and math.max(math.floor((textH - LINE_HEIGHT) / textHeight), 1) or 1
|
|
textFirst = 0
|
|
textDown = true
|
|
textClock = 0
|
|
end
|
|
|
|
|
|
MENU_RENDER.hideGame = function()
|
|
if spriteCabinet then
|
|
spriteUnload(spriteCabinet)
|
|
spriteCabinet = nil
|
|
end
|
|
if spriteMarquee then
|
|
spriteUnload(spriteMarquee)
|
|
spriteMarquee = nil
|
|
end
|
|
for _, handle in ipairs(textSprites) do
|
|
if handle >= 0 then
|
|
spriteUnload(handle)
|
|
end
|
|
end
|
|
for _, handle in ipairs(textShadows) do
|
|
if handle >= 0 then
|
|
spriteUnload(handle)
|
|
end
|
|
end
|
|
textSprites = {}
|
|
textShadows = {}
|
|
end
|
|
|
|
|
|
-- Page up and page down, by as much as is on show less a line to read across. The walk resumes
|
|
-- from where this leaves it, in the direction it was sent, after its usual rest.
|
|
MENU_RENDER.scrollDetails = function(direction)
|
|
local room = #textSprites - textLimit
|
|
|
|
if room > 0 then
|
|
textFirst = math.max(math.min(textFirst + direction * math.max(textLimit - 1, 1), room), 0)
|
|
textDown = (direction > 0)
|
|
textClock = singeGetTicks()
|
|
end
|
|
end
|
|
|
|
|
|
MENU_RENDER.pageSize = function()
|
|
return PAGE_ROWS
|
|
end
|
|
|
|
|
|
MENU_RENDER.frame = function()
|
|
local x = 0
|
|
local y = 0
|
|
|
|
-- Cabinet image
|
|
x = cabinetX + (cabinetW - spriteGetWidth(spriteCabinet)) * 0.5
|
|
y = cabinetY + (cabinetH - spriteGetHeight(spriteCabinet)) * 0.5
|
|
spriteDraw(spriteCabinet, x, y)
|
|
|
|
-- Marquee image
|
|
x = marqueeX + (marqueeW - spriteGetWidth(spriteMarquee)) * 0.5
|
|
y = marqueeY + (marqueeH - spriteGetHeight(spriteMarquee)) * 0.5
|
|
spriteDraw(spriteMarquee, x, y)
|
|
|
|
-- Attract mode video
|
|
if VIDEO_ATTRACT then
|
|
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
|
end
|
|
|
|
-- Which game this is, since there is no list to look at. It heads the details rather than
|
|
-- sitting over the cabinet artwork, where it was unreadable against whatever the picture was.
|
|
local title = string.format("%s (%d of %d)", field(GAME_LIST[GAME_SELECTED].TITLE), GAME_SELECTED, GAME_COUNT)
|
|
|
|
colorForeground(0, 0, 0, 255)
|
|
fontPrint(textX + SHADOW, textY + SHADOW, title)
|
|
colorForeground(255, 211, 90, 255)
|
|
fontPrint(textX, textY, title)
|
|
|
|
-- What the game is. More of it than fits walks a line at a time, down to the end and back up
|
|
-- to the start, so everything an entry says is seen without a key to scroll it -- and without
|
|
-- the jump back to the top that reading the last line used to end in.
|
|
local room = #textSprites - textLimit
|
|
|
|
-- The clock starts when the details are first drawn, not when they were built: the menu is
|
|
-- behind the intro for the first seven seconds, and a page that had already scrolled itself by
|
|
-- the time anybody saw it would appear to start half way down.
|
|
if textClock == 0 then
|
|
textClock = singeGetTicks()
|
|
end
|
|
if room > 0 then
|
|
local resting = (textFirst <= 0) or (textFirst >= room)
|
|
|
|
if (singeGetTicks() - textClock) >= ((resting and SCROLL_HOLD or SCROLL_STEP) * 1000) then
|
|
if textDown and (textFirst >= room) then
|
|
textDown = false
|
|
elseif not textDown and (textFirst <= 0) then
|
|
textDown = true
|
|
end
|
|
textFirst = math.max(math.min(textFirst + (textDown and 1 or -1), room), 0)
|
|
textClock = singeGetTicks()
|
|
end
|
|
end
|
|
|
|
y = textY + LINE_HEIGHT
|
|
for index = textFirst + 1, math.min(textFirst + textLimit, #textSprites) do
|
|
if textSprites[index] >= 0 then
|
|
spriteDraw(textShadows[index], textX + SHADOW, y + SHADOW)
|
|
spriteDraw(textSprites[index], textX, y)
|
|
end
|
|
y = y + textHeight
|
|
end
|
|
|
|
-- Which key does what. The document renderer says the same thing along its footer; here
|
|
-- there was nothing at all, so the only way to learn that up and down move one game and left
|
|
-- and right move a page was to be told.
|
|
fontSelect(hintFont)
|
|
colorForeground(0, 0, 0, 255)
|
|
fontPrint(hintsX + SHADOW, hintsY + SHADOW, HINTS)
|
|
colorForeground(178, 196, 224, 255)
|
|
fontPrint(hintsX, hintsY, HINTS)
|
|
fontSelect(font)
|
|
end
|
|
|
|
|
|
-- A filled rectangle: the overlay has no fill of its own, so it is lines, one a row. The colour
|
|
-- carries its alpha, which is what makes the card a panel over the moving grid and not a slab.
|
|
-- Given a second colour it runs from the first at the top to the second at the bottom: every
|
|
-- row is its own line already, so the gradient costs nothing more.
|
|
local function fillRect(x, y, w, h, colour, toColour)
|
|
local last = math.max(1, h - 1)
|
|
|
|
for row = 0, h - 1 do
|
|
if toColour then
|
|
local t = row / last
|
|
|
|
colorForeground(math.floor(colour[1] + (toColour[1] - colour[1]) * t + 0.5),
|
|
math.floor(colour[2] + (toColour[2] - colour[2]) * t + 0.5),
|
|
math.floor(colour[3] + (toColour[3] - colour[3]) * t + 0.5),
|
|
math.floor(colour[4] + (toColour[4] - colour[4]) * t + 0.5))
|
|
elseif row == 0 then
|
|
colorForeground(colour[1], colour[2], colour[3], colour[4])
|
|
end
|
|
overlayLine(x, y + row, x + w - 1, y + row)
|
|
end
|
|
end
|
|
|
|
|
|
local function ink(colour)
|
|
colorForeground(colour[1], colour[2], colour[3], colour[4])
|
|
end
|
|
|
|
|
|
-- The page: the card with its title bar, the rows in their columns, the chosen row on its bar,
|
|
-- and the key hints in the footer. A page that draws its own picture gets a strip along the
|
|
-- bottom instead; the audio delay tool's flash is the whole screen white and nothing else.
|
|
MENU_RENDER.tools = function()
|
|
if TOOL_FLASH then
|
|
colorBackground(255, 255, 255, 255)
|
|
overlayClear()
|
|
return
|
|
end
|
|
if not TOOL_CLEAR then
|
|
colorBackground(0, 0, 0, 0)
|
|
overlayClear()
|
|
end
|
|
|
|
-- Measuring text means rendering it, so the page is laid out only when a tool says it changed.
|
|
if TOOL_DIRTY then
|
|
TOOL_DIRTY = false
|
|
toolsLayout()
|
|
end
|
|
|
|
local footH = KEY_PAD * 2 + #toolKeyLines * KEY_LINE
|
|
local footY = cardY + cardH - BORDER - footH
|
|
local y = cardY + BORDER + TITLE_H + CARD_PAD
|
|
|
|
-- The card: the panel, its border twice for the width, the title bar, and the footer.
|
|
fillRect(cardX, cardY, cardW, cardH, PANEL, PANEL_DEEP)
|
|
fillRect(cardX + BORDER, cardY + BORDER, cardW - BORDER * 2, TITLE_H, TITLE_BAR_L, TITLE_BAR)
|
|
fillRect(cardX + BORDER, cardY + BORDER + TITLE_H, cardW - BORDER * 2, BORDER, EDGE)
|
|
if #toolKeyLines > 0 then
|
|
fillRect(cardX + BORDER, footY, cardW - BORDER * 2, footH, FOOT)
|
|
fillRect(cardX + BORDER, footY - BORDER, cardW - BORDER * 2, BORDER, EDGE)
|
|
end
|
|
ink(EDGE)
|
|
overlayBox(cardX, cardY, cardX + cardW - 1, cardY + cardH - 1)
|
|
overlayBox(cardX + 1, cardY + 1, cardX + cardW - 2, cardY + cardH - 2)
|
|
if noteX then
|
|
fillRect(noteX - CARD_PAD - BORDER, cardY + BORDER + TITLE_H + BORDER, BORDER, footY - BORDER - (cardY + BORDER + TITLE_H + BORDER), EDGE)
|
|
end
|
|
|
|
fontSelect(titleFont)
|
|
ink(GOLD)
|
|
fontPrint(cardX + BORDER + CARD_PAD, cardY + BORDER + math.floor((TITLE_H - 26) / 2), TOOL_TITLE)
|
|
fontSelect(font)
|
|
|
|
for _, line in ipairs(toolLines) do
|
|
for _, piece in ipairs(line) do
|
|
if piece.bar then
|
|
fillRect(bodyX, y - 3, bodyW, LINE_HEIGHT - 2, TITLE_BAR_L, TITLE_BAR)
|
|
end
|
|
if piece.role == "quiet" then
|
|
ink(QUIET)
|
|
elseif piece.role == "soft" then
|
|
ink(SOFT)
|
|
elseif piece.role == "pick" then
|
|
ink(GOLD)
|
|
else
|
|
ink(INK)
|
|
end
|
|
-- fontPrint refuses an empty string, and a tool legitimately produces one: a status
|
|
-- line with nothing to say yet is still a line, and it still takes up its height.
|
|
if piece.text ~= "" then
|
|
fontPrint(piece.x, y, piece.text)
|
|
end
|
|
end
|
|
y = y + LINE_HEIGHT
|
|
end
|
|
y = cardY + BORDER + TITLE_H + CARD_PAD
|
|
for _, line in ipairs(toolNoteLines) do
|
|
for _, piece in ipairs(line) do
|
|
ink(SOFT)
|
|
if piece.text ~= "" then
|
|
fontPrint(piece.x, y, piece.text)
|
|
end
|
|
end
|
|
y = y + LINE_HEIGHT
|
|
end
|
|
|
|
fontSelect(hintFont)
|
|
ink(QUIET)
|
|
y = footY + KEY_PAD
|
|
for _, line in ipairs(toolKeyLines) do
|
|
if line ~= "" then
|
|
fontPrint(bodyX, y, line)
|
|
end
|
|
y = y + KEY_LINE
|
|
end
|
|
fontSelect(font)
|
|
end
|