mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
lauuncher spruce up part 1
This commit is contained in:
@@ -24,8 +24,12 @@ function love.conf(t)
|
|||||||
local ok, Version = pcall(require, "src.core.Version")
|
local ok, Version = pcall(require, "src.core.Version")
|
||||||
t.window.title = ok and Version.title()
|
t.window.title = ok and Version.title()
|
||||||
or "Pokemon Red (Gen 1 Recompilation Project)"
|
or "Pokemon Red (Gen 1 Recompilation Project)"
|
||||||
t.window.width = 160 * 4
|
-- Open at the launcher's design size (the split-screen ROM selector is
|
||||||
t.window.height = 144 * 4
|
-- laid out for 1024x768). The window is resizable and the 160x144 game
|
||||||
|
-- canvas letterboxes into whatever size it ends up, so this only sets the
|
||||||
|
-- starting size, not the game's resolution.
|
||||||
|
t.window.width = 1024
|
||||||
|
t.window.height = 768
|
||||||
end
|
end
|
||||||
t.version = "11.5"
|
t.version = "11.5"
|
||||||
t.window.vsync = 1
|
t.window.vsync = 1
|
||||||
|
|||||||
@@ -68,20 +68,40 @@ function love.load(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local RomImporter = require("src.import.RomImporter")
|
local RomImporter = require("src.import.RomImporter")
|
||||||
if os.getenv("POKEPORT_FORCE_IMPORT") == "1" or not RomImporter.isReady() then
|
local ready = RomImporter.isReady()
|
||||||
Importer = RomImporter.new(function()
|
local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1"
|
||||||
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
|
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
|
||||||
love.event.quit()
|
-- Scripted / headless runs have to reach the game with no human pressing
|
||||||
return
|
-- Play: an autopilot, a frame driver, an import-only build step, or an
|
||||||
end
|
-- explicit ROM path all bypass the interactive launcher and keep today's
|
||||||
Importer = nil
|
-- import-then-boot (or boot-straight-in) behavior.
|
||||||
bootGame()
|
local scripted = os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER")
|
||||||
end)
|
or os.getenv("POKEPORT_IMPORT_ONLY") == "1" or importPath ~= nil
|
||||||
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
|
|
||||||
if importPath then Importer:startPath(importPath) end
|
if scripted then
|
||||||
|
if forceImport or not ready then
|
||||||
|
Importer = RomImporter.new(function()
|
||||||
|
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
|
||||||
|
love.event.quit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
Importer = nil
|
||||||
|
bootGame()
|
||||||
|
end)
|
||||||
|
if importPath then Importer:startPath(importPath) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
bootGame()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
bootGame()
|
|
||||||
|
-- Interactive: the launcher always runs. Its Red column shows Play when the
|
||||||
|
-- ROM is already imported or Choose ROM / drag-drop when it is not (Blue and
|
||||||
|
-- Yellow are placeholders); pressing Play boots the chosen game.
|
||||||
|
Importer = RomImporter.new(function()
|
||||||
|
Importer = nil
|
||||||
|
bootGame()
|
||||||
|
end, { ready = ready and not forceImport, launcher = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
|
|||||||
@@ -449,6 +449,17 @@ SaveData.addCoreMigration(1, function(save)
|
|||||||
Boxes.ensure(save)
|
Boxes.ensure(save)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- game version (Red vs Blue) prep: saves written before Blue support
|
||||||
|
-- existed carry no `version` tag, and Red is the only game that ever
|
||||||
|
-- shipped, so default every untagged save to Red. from=2 so it catches
|
||||||
|
-- every pre-bump save (format 1 and 2) and is skipped once re-stamped to
|
||||||
|
-- the current format.
|
||||||
|
SaveData.addCoreMigration(2, function(save)
|
||||||
|
if not save.version then
|
||||||
|
save.version = "red"
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
-- ------- write
|
-- ------- write
|
||||||
|
|
||||||
-- Game progress only; options are written separately via saveOptions.
|
-- Game progress only; options are written separately via saveOptions.
|
||||||
@@ -785,6 +796,9 @@ function SaveData.newGame(boot)
|
|||||||
local heal = SaveData.defaultHeal(boot)
|
local heal = SaveData.defaultHeal(boot)
|
||||||
local save = {
|
local save = {
|
||||||
meta = { format = Version.saveFormat, mods = {} },
|
meta = { format = Version.saveFormat, mods = {} },
|
||||||
|
-- which game this playthrough is (Red vs Blue). Only Red ships today;
|
||||||
|
-- boot carries the choice once Blue support lands.
|
||||||
|
version = boot.version or "red",
|
||||||
player = {
|
player = {
|
||||||
map = map,
|
map = map,
|
||||||
x = x,
|
x = x,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ local Version = {
|
|||||||
engine = "1.0.0", -- game/engine release (semver triple)
|
engine = "1.0.0", -- game/engine release (semver triple)
|
||||||
modApi = 2, -- mod API major (manifest `api`)
|
modApi = 2, -- mod API major (manifest `api`)
|
||||||
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
|
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
|
||||||
saveFormat = 2, -- save.meta.format
|
saveFormat = 3, -- save.meta.format
|
||||||
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
|
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+301
-116
@@ -23,6 +23,23 @@ local REQUIRED_FILES = {
|
|||||||
"assets/generated/audio/programs.bin",
|
"assets/generated/audio/programs.bin",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Split-screen ROM-selector palette (the "Split-screen ROM selector" design).
|
||||||
|
-- One column per game: Red is live, Blue and Yellow are placeholders until
|
||||||
|
-- those games are supported. Values are 0-255 RGB.
|
||||||
|
local PAL = {
|
||||||
|
bg = { 241, 243, 232 },
|
||||||
|
detail = { 74, 88, 72 },
|
||||||
|
heading = { 25, 31, 28 },
|
||||||
|
box = { 215, 220, 202 },
|
||||||
|
white = { 255, 255, 255 },
|
||||||
|
red = { 181, 35, 42 },
|
||||||
|
blue = { 30, 86, 168 },
|
||||||
|
gold = { 214, 164, 0 },
|
||||||
|
goldInk = { 160, 120, 0 },
|
||||||
|
disabled = { 146, 158, 178 },
|
||||||
|
disabledInk = { 238, 242, 247 },
|
||||||
|
}
|
||||||
|
|
||||||
local function allRequiredFilesExist()
|
local function allRequiredFilesExist()
|
||||||
-- CacheFs.exists checks the game folder directly for a portable install,
|
-- CacheFs.exists checks the game folder directly for a portable install,
|
||||||
-- otherwise the save directory through love.filesystem.
|
-- otherwise the save directory through love.filesystem.
|
||||||
@@ -205,15 +222,24 @@ local function chooseRom()
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter.new(onComplete)
|
-- onComplete hands off to the game (boot). opts:
|
||||||
|
-- ready -- this game's ROM is already imported: open on the Play state
|
||||||
|
-- launcher -- interactive launcher: a fresh import lands on the Play state
|
||||||
|
-- instead of auto-booting (headless callers omit this and keep
|
||||||
|
-- the old import-then-boot behavior)
|
||||||
|
-- romName -- filename shown next to Play when already imported
|
||||||
|
function RomImporter.new(onComplete, opts)
|
||||||
|
opts = opts or {}
|
||||||
local previousMarker = require("src.import.CacheFs").read(MARKER_PATH)
|
local previousMarker = require("src.import.CacheFs").read(MARKER_PATH)
|
||||||
local returning = previousMarker ~= nil and previousMarker ~= CACHE_MARKER
|
local returning = previousMarker ~= nil and previousMarker ~= CACHE_MARKER
|
||||||
local android = love.system.getOS() == "Android"
|
local android = love.system.getOS() == "Android"
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
onComplete = onComplete,
|
onComplete = onComplete,
|
||||||
|
launcher = opts.launcher or false,
|
||||||
logo = love.graphics.newImage("assets/logo/logo.png"),
|
logo = love.graphics.newImage("assets/logo/logo.png"),
|
||||||
bcg = love.graphics.newImage("assets/logo/bcg.png"),
|
bcg = love.graphics.newImage("assets/logo/bcg.png"),
|
||||||
state = "waiting",
|
state = opts.ready and "ready" or "waiting",
|
||||||
|
romName = opts.romName or "pokemon_red.gb",
|
||||||
returning = returning,
|
returning = returning,
|
||||||
android = android,
|
android = android,
|
||||||
status = returning and "More assets are needed from your ROM"
|
status = returning and "More assets are needed from your ROM"
|
||||||
@@ -226,10 +252,11 @@ function RomImporter.new(onComplete)
|
|||||||
stageCurrent = 0,
|
stageCurrent = 0,
|
||||||
stageTotal = 1,
|
stageTotal = 1,
|
||||||
pulse = 0,
|
pulse = 0,
|
||||||
button = {},
|
|
||||||
}, RomImporter)
|
}, RomImporter)
|
||||||
|
|
||||||
if android then
|
-- Only hunt for a ROM when one is actually needed; an already-imported
|
||||||
|
-- game opens straight on Play.
|
||||||
|
if android and self.state ~= "ready" then
|
||||||
self.status = returning and "More ROM assets needed" or "Get your Pokemon Red ROM (.gb) in"
|
self.status = returning and "More ROM assets needed" or "Get your Pokemon Red ROM (.gb) in"
|
||||||
self.detail = "Tap Choose ROM to pick your file"
|
self.detail = "Tap Choose ROM to pick your file"
|
||||||
local name = scanForRom()
|
local name = scanForRom()
|
||||||
@@ -247,7 +274,10 @@ end
|
|||||||
-- it into the folder scanForRom checks, so a rescan on refocus picks it up
|
-- it into the folder scanForRom checks, so a rescan on refocus picks it up
|
||||||
-- without the player needing to tap the button again.
|
-- without the player needing to tap the button again.
|
||||||
function RomImporter:focus(f)
|
function RomImporter:focus(f)
|
||||||
if not (f and self.android and self.state ~= "working") then return end
|
if not (f and self.android
|
||||||
|
and (self.state == "waiting" or self.state == "error")) then
|
||||||
|
return
|
||||||
|
end
|
||||||
local name = scanForRom()
|
local name = scanForRom()
|
||||||
if name then
|
if name then
|
||||||
self:startData(love.filesystem.read(name), name)
|
self:startData(love.filesystem.read(name), name)
|
||||||
@@ -321,6 +351,14 @@ function RomImporter:startData(data, displayName)
|
|||||||
self.status = "Ready"
|
self.status = "Ready"
|
||||||
self.detail = "Starting Pokemon Red..."
|
self.detail = "Starting Pokemon Red..."
|
||||||
self.progress = 1
|
self.progress = 1
|
||||||
|
if self.launcher then
|
||||||
|
-- Stay on the launcher and show Play for the game just imported; the
|
||||||
|
-- player presses Play to boot it.
|
||||||
|
self.romName = (displayName and (displayName:match("[^/\\]+$") or displayName))
|
||||||
|
or self.romName
|
||||||
|
self.state = "ready"
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.onComplete then self.onComplete() end
|
if self.onComplete then self.onComplete() end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -391,146 +429,293 @@ function RomImporter:update(dt)
|
|||||||
until love.timer.getTime() - started >= 0.008
|
until love.timer.getTime() - started >= 0.008
|
||||||
end
|
end
|
||||||
|
|
||||||
local function setColor255(r, g, b, a)
|
-- Player pressed Play on a game whose ROM is imported: hand off to boot.
|
||||||
love.graphics.setColor(r / 255, g / 255, b / 255, (a or 255) / 255)
|
function RomImporter:play()
|
||||||
|
if self.onComplete then self.onComplete() end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function printCentered(text, y, font, width)
|
-- "re-import" from the Play state: drop back to the choose/drop UI so a fresh
|
||||||
love.graphics.setFont(font)
|
-- ROM can be selected (the extract itself replaces the old cache).
|
||||||
love.graphics.printf(text, 0, y, width, "center")
|
function RomImporter:reimport()
|
||||||
|
if self.state ~= "ready" then return end
|
||||||
|
self.state = "waiting"
|
||||||
|
self.returning = false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clamp(v, lo, hi)
|
||||||
|
return math.max(lo, math.min(hi, v))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the current draw color from a PAL triple (0-255), with optional alpha 0-1
|
||||||
|
local function col(c, a)
|
||||||
|
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, a or 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- LOVE has no dashed-stroke primitive; step short segments along each edge so
|
||||||
|
-- the "coming soon" Blue/Yellow boxes read as placeholders
|
||||||
|
local function dashedRect(x, y, w, h, dash, gap)
|
||||||
|
local step = dash + gap
|
||||||
|
local function edge(x1, y1, x2, y2)
|
||||||
|
local dx, dy = x2 - x1, y2 - y1
|
||||||
|
local len = math.sqrt(dx * dx + dy * dy)
|
||||||
|
if len == 0 then return end
|
||||||
|
local ux, uy = dx / len, dy / len
|
||||||
|
local d = 0
|
||||||
|
while d < len do
|
||||||
|
local e = math.min(d + dash, len)
|
||||||
|
love.graphics.line(x1 + ux * d, y1 + uy * d, x1 + ux * e, y1 + uy * e)
|
||||||
|
d = d + step
|
||||||
|
end
|
||||||
|
end
|
||||||
|
edge(x, y, x + w, y)
|
||||||
|
edge(x + w, y, x + w, y + h)
|
||||||
|
edge(x + w, y + h, x, y + h)
|
||||||
|
edge(x, y + h, x, y)
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:draw()
|
function RomImporter:draw()
|
||||||
local width, height = love.graphics.getDimensions()
|
local width, height = love.graphics.getDimensions()
|
||||||
setColor255(241, 243, 232)
|
local third = width / 3
|
||||||
|
col(PAL.bg)
|
||||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||||
setColor255(181, 35, 42)
|
|
||||||
love.graphics.rectangle("fill", 0, 0, width, math.max(8, height * 0.025))
|
|
||||||
|
|
||||||
|
-- top tri-colour stripe (Red | Blue | Yellow)
|
||||||
|
local stripeH = math.max(8, height * 0.02)
|
||||||
|
col(PAL.red); love.graphics.rectangle("fill", 0, 0, third, stripeH)
|
||||||
|
col(PAL.blue); love.graphics.rectangle("fill", third, 0, third, stripeH)
|
||||||
|
col(PAL.gold); love.graphics.rectangle("fill", 2 * third, 0, width - 2 * third, stripeH)
|
||||||
|
|
||||||
|
-- fonts, rebuilt only when the window size changes
|
||||||
local fontKey = ("%dx%d"):format(width, height)
|
local fontKey = ("%dx%d"):format(width, height)
|
||||||
if self.fontKey ~= fontKey then
|
if self.fontKey ~= fontKey then
|
||||||
self.fontKey = fontKey
|
self.fontKey = fontKey
|
||||||
self.bodyFont = love.graphics.newFont(
|
local function f(frac, lo, hi) return love.graphics.newFont(clamp(height * frac, lo, hi)) end
|
||||||
math.max(16, math.min(22, height * 0.038)))
|
self.pillFont = f(0.021, 12, 15)
|
||||||
self.smallFont = love.graphics.newFont(
|
self.headFont = f(0.030, 16, 22)
|
||||||
math.max(13, math.min(17, height * 0.029)))
|
self.detailFont = f(0.023, 13, 16)
|
||||||
self.warningFont = love.graphics.newFont(
|
self.buttonFont = f(0.029, 16, 22)
|
||||||
math.max(10, math.min(12, height * 0.022)))
|
self.hintFont = f(0.021, 12, 15)
|
||||||
|
self.warningFont = f(0.017, 10, 12)
|
||||||
end
|
end
|
||||||
local bodyFont, smallFont, warningFont =
|
|
||||||
self.bodyFont, self.smallFont, self.warningFont
|
|
||||||
local contentWidth = math.min(width - 40, 520)
|
|
||||||
local left = (width - contentWidth) / 2
|
|
||||||
|
|
||||||
local logoWidth, logoHeight = self.logo:getDimensions()
|
-- footer (Boi's Club Games logo + trust warning), pinned to the bottom and
|
||||||
local logoScale = math.min(
|
-- measured first so the columns know where they must stop
|
||||||
math.min(width - 48, 420) / logoWidth,
|
local warningWidth = math.min(width - 32, 620)
|
||||||
height * 0.15 / logoHeight)
|
love.graphics.setFont(self.warningFont)
|
||||||
|
local _, warningLines = self.warningFont:getWrap(TRUST_WARNING, warningWidth)
|
||||||
|
local warningH = #warningLines * self.warningFont:getHeight()
|
||||||
|
local warningY = height - warningH - 8
|
||||||
|
local bcgW, bcgH = self.bcg:getDimensions()
|
||||||
|
local bcgScale = math.min(math.min(width - 48, 200) / bcgW, height * 0.075 / bcgH)
|
||||||
|
local bcgDW, bcgDH = bcgW * bcgScale, bcgH * bcgScale
|
||||||
|
local bcgX, bcgY = (width - bcgDW) / 2, warningY - bcgDH - 8
|
||||||
|
self.bcgButton = { x = bcgX, y = bcgY, width = bcgDW, height = bcgDH }
|
||||||
|
local footerTop = bcgY - 10
|
||||||
|
|
||||||
|
-- faint per-column tints under the split
|
||||||
|
col(PAL.red, 0.07); love.graphics.rectangle("fill", 0, stripeH, third, footerTop - stripeH)
|
||||||
|
col(PAL.blue, 0.07); love.graphics.rectangle("fill", third, stripeH, third, footerTop - stripeH)
|
||||||
|
col(PAL.gold, 0.08); love.graphics.rectangle("fill", 2 * third, stripeH, width - 2 * third, footerTop - stripeH)
|
||||||
|
|
||||||
|
-- logo (larger, no subtitle), centred over the split
|
||||||
|
local logoW, logoH = self.logo:getDimensions()
|
||||||
|
local logoScale = math.min(math.min(width - 40, 560) / logoW, height * 0.20 / logoH)
|
||||||
|
local logoDW, logoDH = logoW * logoScale, logoH * logoScale
|
||||||
|
local logoY = stripeH + height * 0.02
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(
|
love.graphics.draw(self.logo, (width - logoDW) / 2, logoY, 0, logoScale, logoScale)
|
||||||
self.logo,
|
|
||||||
(width - logoWidth * logoScale) / 2,
|
|
||||||
height * 0.075,
|
|
||||||
0, logoScale, logoScale)
|
|
||||||
setColor255(74, 88, 72)
|
|
||||||
printCentered(self.returning and "UPDATE REQUIRED" or "FIRST RUN",
|
|
||||||
height * 0.205, smallFont, width)
|
|
||||||
|
|
||||||
local zoneY, zoneH = height * 0.29, math.min(180, height * 0.31)
|
-- shared column metrics so the three columns line up exactly
|
||||||
setColor255(215, 220, 202)
|
local m = {
|
||||||
love.graphics.rectangle("fill", left, zoneY, contentWidth, zoneH)
|
boxW = math.min(third - 32, 300),
|
||||||
setColor255(74, 88, 72)
|
boxH = clamp(height * 0.22, 120, 190),
|
||||||
love.graphics.setLineWidth(2)
|
pillH = self.pillFont:getHeight() + 10,
|
||||||
love.graphics.rectangle("line", left, zoneY, contentWidth, zoneH)
|
buttonH = clamp(height * 0.072, 44, 56),
|
||||||
|
gap1 = clamp(height * 0.028, 12, 22),
|
||||||
setColor255(25, 31, 28)
|
gap2 = clamp(height * 0.030, 14, 24),
|
||||||
printCentered(self.status, zoneY + zoneH * 0.25, bodyFont, width)
|
gap3 = 10,
|
||||||
setColor255(74, 88, 72)
|
hintH = self.hintFont:getHeight(),
|
||||||
love.graphics.setFont(smallFont)
|
|
||||||
local _, wrapped = smallFont:getWrap(self.detail, contentWidth - 48)
|
|
||||||
local visible = {}
|
|
||||||
for index = 1, math.min(#wrapped, 3) do visible[index] = wrapped[index] end
|
|
||||||
love.graphics.printf(table.concat(visible, "\n"),
|
|
||||||
left + 24, zoneY + zoneH * 0.52, contentWidth - 48, "center")
|
|
||||||
|
|
||||||
if self.state == "working" or self.state == "complete" then
|
|
||||||
local barY = zoneY + zoneH - 24
|
|
||||||
setColor255(164, 172, 151)
|
|
||||||
love.graphics.rectangle("fill", left + 24, barY, contentWidth - 48, 8)
|
|
||||||
setColor255(181, 35, 42)
|
|
||||||
love.graphics.rectangle("fill", left + 24, barY,
|
|
||||||
(contentWidth - 48) * self.progress, 8)
|
|
||||||
else
|
|
||||||
local buttonWidth = math.min(260, contentWidth - 80)
|
|
||||||
local buttonHeight = math.max(46, math.min(56, height * 0.09))
|
|
||||||
local buttonX = (width - buttonWidth) / 2
|
|
||||||
local buttonY = math.min(height - buttonHeight - 34, zoneY + zoneH + 36)
|
|
||||||
self.button = {
|
|
||||||
x = buttonX, y = buttonY, width = buttonWidth, height = buttonHeight,
|
|
||||||
}
|
|
||||||
setColor255(25, 31, 28)
|
|
||||||
love.graphics.rectangle("fill", buttonX, buttonY, buttonWidth, buttonHeight)
|
|
||||||
setColor255(255, 255, 255)
|
|
||||||
love.graphics.setFont(bodyFont)
|
|
||||||
love.graphics.printf("Choose ROM", buttonX,
|
|
||||||
buttonY + (buttonHeight - bodyFont:getHeight()) / 2,
|
|
||||||
buttonWidth, "center")
|
|
||||||
setColor255(74, 88, 72)
|
|
||||||
love.graphics.setFont(smallFont)
|
|
||||||
love.graphics.printf(
|
|
||||||
self.android and "or copy the .gb via USB" or "or drop the .gb file here",
|
|
||||||
0, buttonY + buttonHeight + 12, width, "center")
|
|
||||||
end
|
|
||||||
|
|
||||||
local bcgWidth, bcgHeight = self.bcg:getDimensions()
|
|
||||||
love.graphics.setFont(warningFont)
|
|
||||||
local warningWidth = math.min(width - 32, 600)
|
|
||||||
local _, warningLines = warningFont:getWrap(TRUST_WARNING, warningWidth)
|
|
||||||
local warningHeight = #warningLines * warningFont:getHeight()
|
|
||||||
local warningY = height - warningHeight - 8
|
|
||||||
local bcgScale = math.min(
|
|
||||||
math.min(width - 48, 220) / bcgWidth,
|
|
||||||
height * 0.08 / bcgHeight)
|
|
||||||
local bcgDrawWidth = bcgWidth * bcgScale
|
|
||||||
local bcgDrawHeight = bcgHeight * bcgScale
|
|
||||||
local bcgX = (width - bcgDrawWidth) / 2
|
|
||||||
local bcgY = warningY - bcgDrawHeight - 8
|
|
||||||
self.bcgButton = {
|
|
||||||
x = bcgX, y = bcgY,
|
|
||||||
width = bcgDrawWidth, height = bcgDrawHeight,
|
|
||||||
}
|
}
|
||||||
|
local stackH = m.pillH + m.gap1 + m.boxH + m.gap2 + m.buttonH + m.gap3 + m.hintH
|
||||||
|
local regionTop = logoY + logoDH + height * 0.03
|
||||||
|
local top = regionTop + math.max(0, (footerTop - regionTop - stackH) / 2)
|
||||||
|
|
||||||
|
-- draw one column and return the button/link hit rects for the caller
|
||||||
|
local function column(colX, spec)
|
||||||
|
local cx = colX + third / 2
|
||||||
|
local x = cx - m.boxW / 2
|
||||||
|
local a = spec.alpha or 1
|
||||||
|
local y = top
|
||||||
|
|
||||||
|
-- pill badge
|
||||||
|
love.graphics.setFont(self.pillFont)
|
||||||
|
local pillW = self.pillFont:getWidth(spec.label) + 28
|
||||||
|
col(spec.color, a)
|
||||||
|
love.graphics.setLineWidth(2)
|
||||||
|
love.graphics.rectangle("line", cx - pillW / 2, y, pillW, m.pillH, m.pillH / 2, m.pillH / 2)
|
||||||
|
love.graphics.printf(spec.label, cx - pillW / 2,
|
||||||
|
y + (m.pillH - self.pillFont:getHeight()) / 2, pillW, "center")
|
||||||
|
y = y + m.pillH + m.gap1
|
||||||
|
|
||||||
|
-- box
|
||||||
|
col(PAL.box, a)
|
||||||
|
love.graphics.rectangle("fill", x, y, m.boxW, m.boxH)
|
||||||
|
love.graphics.setLineWidth(2)
|
||||||
|
col(spec.color, a)
|
||||||
|
if spec.dashed then
|
||||||
|
dashedRect(x, y, m.boxW, m.boxH, 7, 5)
|
||||||
|
else
|
||||||
|
love.graphics.rectangle("line", x, y, m.boxW, m.boxH)
|
||||||
|
end
|
||||||
|
local pad = 14
|
||||||
|
love.graphics.setFont(self.headFont)
|
||||||
|
col(PAL.heading, a)
|
||||||
|
love.graphics.printf(spec.heading, x + pad, y + pad, m.boxW - 2 * pad, "center")
|
||||||
|
local _, headLines = self.headFont:getWrap(spec.heading, m.boxW - 2 * pad)
|
||||||
|
local headBlock = math.max(1, #headLines) * self.headFont:getHeight()
|
||||||
|
love.graphics.setFont(self.detailFont)
|
||||||
|
col(PAL.detail, a)
|
||||||
|
love.graphics.printf(spec.detail, x + pad, y + pad + headBlock + 8, m.boxW - 2 * pad, "center")
|
||||||
|
if spec.progress then
|
||||||
|
local barY = y + m.boxH - 20
|
||||||
|
col(PAL.disabled, a)
|
||||||
|
love.graphics.rectangle("fill", x + pad, barY, m.boxW - 2 * pad, 8)
|
||||||
|
col(spec.color, a)
|
||||||
|
love.graphics.rectangle("fill", x + pad, barY, (m.boxW - 2 * pad) * spec.progress, 8)
|
||||||
|
end
|
||||||
|
y = y + m.boxH + m.gap2
|
||||||
|
|
||||||
|
-- button
|
||||||
|
local rect
|
||||||
|
if spec.button then
|
||||||
|
rect = { x = x, y = y, width = m.boxW, height = m.buttonH }
|
||||||
|
col(spec.button.bg, a)
|
||||||
|
love.graphics.rectangle("fill", x, y, m.boxW, m.buttonH)
|
||||||
|
love.graphics.setFont(self.buttonFont)
|
||||||
|
local label = spec.button.text
|
||||||
|
if spec.button.play then
|
||||||
|
-- filled play triangle + label, centred as a group
|
||||||
|
local tw = self.buttonFont:getWidth(label)
|
||||||
|
local tri = self.buttonFont:getHeight() * 0.55
|
||||||
|
local groupW = tri + 12 + tw
|
||||||
|
local gx = x + (m.boxW - groupW) / 2
|
||||||
|
local gy = y + m.buttonH / 2
|
||||||
|
col(spec.button.fg, a)
|
||||||
|
love.graphics.polygon("fill", gx, gy - tri / 2, gx, gy + tri / 2, gx + tri * 0.9, gy)
|
||||||
|
love.graphics.print(label, gx + tri + 12,
|
||||||
|
y + (m.buttonH - self.buttonFont:getHeight()) / 2)
|
||||||
|
else
|
||||||
|
col(spec.button.fg, a)
|
||||||
|
love.graphics.printf(label, x,
|
||||||
|
y + (m.buttonH - self.buttonFont:getHeight()) / 2, m.boxW, "center")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
y = y + m.buttonH + m.gap3
|
||||||
|
|
||||||
|
-- hint line, or the "<rom> imported · re-import" link
|
||||||
|
love.graphics.setFont(self.hintFont)
|
||||||
|
local reimportRect
|
||||||
|
if spec.reimport then
|
||||||
|
local prefix = spec.romName .. " imported · "
|
||||||
|
local link = "re-import"
|
||||||
|
local pw = self.hintFont:getWidth(prefix)
|
||||||
|
local lw = self.hintFont:getWidth(link)
|
||||||
|
local startX = cx - (pw + lw) / 2
|
||||||
|
col(PAL.detail, a)
|
||||||
|
love.graphics.print(prefix, startX, y)
|
||||||
|
love.graphics.print(link, startX + pw, y)
|
||||||
|
love.graphics.setLineWidth(1)
|
||||||
|
love.graphics.line(startX + pw, y + self.hintFont:getHeight(),
|
||||||
|
startX + pw + lw, y + self.hintFont:getHeight())
|
||||||
|
reimportRect = { x = startX + pw, y = y, width = lw, height = m.hintH + 2 }
|
||||||
|
elseif spec.hint then
|
||||||
|
col(PAL.detail, a)
|
||||||
|
love.graphics.printf(spec.hint, x, y, m.boxW, "center")
|
||||||
|
end
|
||||||
|
|
||||||
|
return rect, reimportRect
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Red column: live, driven by the import state machine
|
||||||
|
local redHint = self.android and "or copy the .gb via USB" or "or drop the .gb file here"
|
||||||
|
local redSpec = { color = PAL.red, label = "RED", dashed = false, alpha = 1 }
|
||||||
|
if self.state == "ready" then
|
||||||
|
redSpec.heading = "Red ROM ready"
|
||||||
|
redSpec.detail = "Your ROM is verified. Press Play to start."
|
||||||
|
redSpec.button = { text = "Play Red", bg = PAL.red, fg = PAL.white, play = true }
|
||||||
|
redSpec.reimport = true
|
||||||
|
redSpec.romName = self.romName
|
||||||
|
elseif self.state == "working" or self.state == "complete" then
|
||||||
|
redSpec.heading = self.status
|
||||||
|
redSpec.detail = self.detail
|
||||||
|
redSpec.progress = self.progress or 0
|
||||||
|
elseif self.state == "error" then
|
||||||
|
redSpec.heading = "That ROM could not be imported"
|
||||||
|
redSpec.detail = self.detail
|
||||||
|
redSpec.button = { text = "Choose ROM", bg = PAL.red, fg = PAL.white }
|
||||||
|
redSpec.hint = redHint
|
||||||
|
else -- waiting
|
||||||
|
if self.android then
|
||||||
|
redSpec.heading = self.status
|
||||||
|
redSpec.detail = self.detail
|
||||||
|
elseif self.returning then
|
||||||
|
redSpec.heading = "Update required"
|
||||||
|
redSpec.detail = "This build needs a few more things from your ROM. Re-import to continue."
|
||||||
|
else
|
||||||
|
redSpec.heading = "Choose or drop a Red ROM"
|
||||||
|
redSpec.detail = "The ROM is verified before any files are created."
|
||||||
|
end
|
||||||
|
redSpec.button = { text = "Choose ROM", bg = PAL.red, fg = PAL.white }
|
||||||
|
redSpec.hint = redHint
|
||||||
|
end
|
||||||
|
self.redButton, self.reimportRect = column(0, redSpec)
|
||||||
|
|
||||||
|
-- Blue and Yellow columns: placeholders until those games are supported
|
||||||
|
column(third, {
|
||||||
|
color = PAL.blue, label = "BLUE", dashed = true, alpha = 0.72,
|
||||||
|
heading = "Choose or drop a Blue ROM", detail = "Blue support is on the way.",
|
||||||
|
button = { text = "Coming soon", bg = PAL.disabled, fg = PAL.disabledInk },
|
||||||
|
hint = "not yet available",
|
||||||
|
})
|
||||||
|
column(2 * third, {
|
||||||
|
color = PAL.gold, label = "YELLOW", dashed = true, alpha = 0.72,
|
||||||
|
heading = "Choose or drop a Yellow ROM", detail = "Yellow support is on the way.",
|
||||||
|
button = { text = "Coming soon", bg = PAL.disabled, fg = PAL.disabledInk },
|
||||||
|
hint = "not yet available",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- footer
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(
|
love.graphics.draw(self.bcg, bcgX, bcgY, 0, bcgScale, bcgScale)
|
||||||
self.bcg,
|
col(PAL.detail)
|
||||||
bcgX, bcgY,
|
love.graphics.setFont(self.warningFont)
|
||||||
0, bcgScale, bcgScale)
|
love.graphics.printf(TRUST_WARNING, (width - warningWidth) / 2, warningY, warningWidth, "center")
|
||||||
setColor255(74, 88, 72)
|
|
||||||
love.graphics.printf(
|
|
||||||
TRUST_WARNING,
|
|
||||||
(width - warningWidth) / 2, warningY,
|
|
||||||
warningWidth, "center")
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function inside(r, x, y)
|
||||||
|
return r and x >= r.x and x <= r.x + r.width and y >= r.y and y <= r.y + r.height
|
||||||
|
end
|
||||||
|
|
||||||
function RomImporter:mousepressed(x, y, button)
|
function RomImporter:mousepressed(x, y, button)
|
||||||
if button ~= 1 then return end
|
if button ~= 1 then return end
|
||||||
local logo = self.bcgButton or {}
|
if inside(self.bcgButton, x, y) then
|
||||||
if x >= (logo.x or 0) and x <= (logo.x or 0) + (logo.width or 0)
|
|
||||||
and y >= (logo.y or 0) and y <= (logo.y or 0) + (logo.height or 0) then
|
|
||||||
love.system.openURL(COMMUNITY_URL)
|
love.system.openURL(COMMUNITY_URL)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self.state == "working" then return end
|
if self.state == "working" or self.state == "complete" then return end
|
||||||
local rect = self.button
|
if self.state == "ready" then
|
||||||
if x >= (rect.x or 0) and x <= (rect.x or 0) + (rect.width or 0)
|
if inside(self.reimportRect, x, y) then self:reimport()
|
||||||
and y >= (rect.y or 0) and y <= (rect.y or 0) + (rect.height or 0) then
|
elseif inside(self.redButton, x, y) then self:play() end
|
||||||
self:choose()
|
return
|
||||||
end
|
end
|
||||||
|
if inside(self.redButton, x, y) then self:choose() end
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:keypressed(key)
|
function RomImporter:keypressed(key)
|
||||||
if (key == "return" or key == "space") and self.state ~= "working" then
|
if self.state == "working" or self.state == "complete" then return end
|
||||||
self:choose()
|
if key == "return" or key == "space" or key == "kpenter" then
|
||||||
|
if self.state == "ready" then self:play() else self:choose() end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ do
|
|||||||
check(#loaded.meta.mods == 0, "old vanilla save records an empty mod set")
|
check(#loaded.meta.mods == 0, "old vanilla save records an empty mod set")
|
||||||
check(loaded.boxes and loaded.boxes[1][1].species == "PIDGEY", "box list folded into boxes[1]")
|
check(loaded.boxes and loaded.boxes[1][1].species == "PIDGEY", "box list folded into boxes[1]")
|
||||||
check(loaded.box == nil, "legacy box key gone")
|
check(loaded.box == nil, "legacy box key gone")
|
||||||
|
check(loaded.version == "red", "untagged save defaults to the Red version")
|
||||||
local opts = SaveSerializer.decode(files["options.lua"])
|
local opts = SaveSerializer.decode(files["options.lua"])
|
||||||
check(opts and opts.musicVol == 2, "embedded options split into options.lua")
|
check(opts and opts.musicVol == 2, "embedded options split into options.lua")
|
||||||
|
|
||||||
@@ -203,7 +204,16 @@ do
|
|||||||
local current = { meta = { format = Version.saveFormat, mods = {} },
|
local current = { meta = { format = Version.saveFormat, mods = {} },
|
||||||
player = { map = "PALLET_TOWN", x = 1, y = 1 } }
|
player = { map = "PALLET_TOWN", x = 1, y = 1 } }
|
||||||
SaveData.runMigrations(current)
|
SaveData.runMigrations(current)
|
||||||
check(current.player.id == nil, "format-2 save skips the id backfill")
|
check(current.player.id == nil, "current-format save skips the id backfill")
|
||||||
|
|
||||||
|
-- a format-2 save (Blue support not yet shipped) gains the Red tag;
|
||||||
|
-- a save that already names a version keeps it
|
||||||
|
local untagged = { meta = { format = 2, mods = {} }, player = {} }
|
||||||
|
SaveData.runMigrations(untagged)
|
||||||
|
check(untagged.version == "red", "pre-version save defaults to Red")
|
||||||
|
local tagged = { meta = { format = 2, mods = {} }, version = "blue", player = {} }
|
||||||
|
SaveData.runMigrations(tagged)
|
||||||
|
check(tagged.version == "blue", "an existing version tag is left untouched")
|
||||||
|
|
||||||
love.filesystem = realFS
|
love.filesystem = realFS
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user