mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
Merge branch 'my-uncles-neighbor' into dev
# Conflicts: # src/import/RomImporter.lua
This commit is contained in:
+2
-2
@@ -3,7 +3,7 @@
|
||||
-- everything under src/*/gen2 reaches shared services through here. Gen 1
|
||||
-- Game:load cannot consume a Gen 2 cache -- different generated tables, save
|
||||
-- shape and screen registry -- so main.lua's bootGame picks this owner when
|
||||
-- GameVersion.isGold(), and the two never branch into each other.
|
||||
-- GameVersion.generation() == 2, and the two never branch into each other.
|
||||
--
|
||||
-- Boot: copyright → GameFreak Presents → GS intro stub → title
|
||||
-- (tilemap + Ho-Oh flap / clouds / trails) → Oak speech (Marill + shrink)
|
||||
@@ -1702,7 +1702,7 @@ function Game2:hotkey(key)
|
||||
self:writeSave()
|
||||
return true
|
||||
elseif key == "f2" then
|
||||
local loaded = Save.load("gold")
|
||||
local loaded = Save.load()
|
||||
if loaded then self:continueGame(loaded) end
|
||||
return true
|
||||
elseif key == "1" then
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Which game this process is running: Red (the historical default), Blue,
|
||||
-- Yellow, or Gold. One source of truth for everything that differs by
|
||||
-- Yellow, Gold, or Silver. One source of truth for everything that differs by
|
||||
-- version -- the accepted ROM hash, the import manifest, where the
|
||||
-- extracted cache lives, and the save-file suffix -- so the importer,
|
||||
-- cache mount, SaveData, title screen and palette all agree.
|
||||
@@ -8,7 +8,8 @@
|
||||
-- saves are untouched, but its extracted cache lives under red/ like Blue,
|
||||
-- Yellow, and Gold (issue #899); a legacy root cache is moved into red/ once
|
||||
-- by CacheFs.migrateLegacyRedCache. All supported versions can be imported
|
||||
-- and selected side by side. Gold is Gen 2 (see docs/gold-phase1.md).
|
||||
-- and selected side by side. Gold and Silver are Gen 2 (see
|
||||
-- docs/gold-phase1.md).
|
||||
--
|
||||
-- Zero requires, so it loads during love.conf and under plain Lua for tools
|
||||
-- and tests. The active version is a process-global set once at boot from
|
||||
@@ -61,13 +62,26 @@ GameVersion.VERSIONS = {
|
||||
manifest = "tools/rom_manifest_gold.json",
|
||||
cachePrefix = "gold/", -- gold/data/generated, gold/assets/generated
|
||||
saveSuffix = "_gold", -- save_gold.lua / .bak / .tmp
|
||||
-- The only row that carries one; absent reads as 1 (GameVersion.generation)
|
||||
-- Absent reads as 1 (GameVersion.generation)
|
||||
generation = 2,
|
||||
},
|
||||
-- Gold's engine with edition-selected data; the manifest is derived from
|
||||
-- Gold's by tools/make_silver_manifest.py.
|
||||
silver = {
|
||||
id = "silver",
|
||||
label = "Silver",
|
||||
displayName = "Pokemon Silver",
|
||||
launcherName = "Silver (Beta)",
|
||||
sha1 = "49b163f7e57702bc939d642a18f591de55d92dae",
|
||||
manifest = "tools/rom_manifest_silver.json",
|
||||
cachePrefix = "silver/", -- silver/data/generated, silver/assets/generated
|
||||
saveSuffix = "_silver", -- save_silver.lua / .bak / .tmp
|
||||
generation = 2,
|
||||
},
|
||||
}
|
||||
|
||||
-- Launcher column order.
|
||||
GameVersion.ORDER = { "red", "blue", "yellow", "gold" }
|
||||
GameVersion.ORDER = { "red", "blue", "yellow", "gold", "silver" }
|
||||
|
||||
GameVersion.current = "red"
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ local function normalizeVersion(v)
|
||||
b = "blue", blue = "blue",
|
||||
y = "yellow", yellow = "yellow",
|
||||
g = "gold", gold = "gold",
|
||||
s = "silver", silver = "silver",
|
||||
}
|
||||
v = alias[v] or v
|
||||
if GameVersion.VERSIONS and not GameVersion.VERSIONS[v] then return nil end
|
||||
|
||||
@@ -218,7 +218,7 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
|
||||
local ssW = dpadW * 0.30
|
||||
local margin = dpadW * 0.12
|
||||
local ok, GameVersion = pcall(require, "src.core.GameVersion")
|
||||
if ok and GameVersion.isGold and GameVersion.isGold() then
|
||||
if ok and GameVersion.generation and GameVersion.generation() == 2 then
|
||||
margin = math.max(margin, math.min(ww * 0.10, 72))
|
||||
end
|
||||
return {
|
||||
|
||||
+24
-12
@@ -103,7 +103,7 @@ Save.PLAYER_STATES = {
|
||||
}
|
||||
|
||||
local function saveNames(version)
|
||||
version = version or "gold"
|
||||
version = version or GameVersion.get()
|
||||
-- Resolve the ACTIVE SLOT the same way SaveData does, and only fall back to
|
||||
-- the flat save_<version>.lua when no slot is registered.
|
||||
--
|
||||
@@ -133,16 +133,22 @@ local function fs()
|
||||
return love.filesystem
|
||||
end
|
||||
|
||||
-- A fresh Gold save. `opts` carries what the intro collected: player name,
|
||||
-- The blank-name fallback is the first PlayerNameArray row, which differs
|
||||
-- per edition -- data/player_names.asm:12-23.
|
||||
function Save.defaultPlayerName(version)
|
||||
return (version or GameVersion.get()) == "silver" and "SILVER" or "GOLD"
|
||||
end
|
||||
|
||||
-- A fresh Gen 2 save. `opts` carries what the intro collected: player name,
|
||||
-- rival name, and the options the OPTION screen was left on.
|
||||
function Save.newGame(opts)
|
||||
opts = opts or {}
|
||||
local save = {
|
||||
format = Save.FORMAT,
|
||||
version = "gold",
|
||||
version = GameVersion.get(),
|
||||
generation = 2,
|
||||
player = {
|
||||
name = opts.playerName or "GOLD",
|
||||
name = opts.playerName or Save.defaultPlayerName(),
|
||||
-- _ResetWRAM rolls wPlayerID out of hRandomSub/hRandomAdd
|
||||
-- (engine/menus/intro_menu.asm:41-49).
|
||||
id = opts.trainerId or rand(0, 65535),
|
||||
@@ -374,10 +380,13 @@ end
|
||||
function Save.normalize(save)
|
||||
if type(save) ~= "table" then return nil end
|
||||
save.format = save.format or Save.FORMAT
|
||||
save.version = "gold"
|
||||
if not (GameVersion.VERSIONS[save.version]
|
||||
and GameVersion.generation(save.version) == 2) then
|
||||
save.version = GameVersion.get()
|
||||
end
|
||||
save.generation = 2
|
||||
save.player = save.player or {}
|
||||
save.player.name = save.player.name or "GOLD"
|
||||
save.player.name = save.player.name or Save.defaultPlayerName(save.version)
|
||||
save.player.id = save.player.id or rand(0, 65535)
|
||||
save.player.money = math.max(0, math.min(save.player.money or 0, Save.MAX_MONEY))
|
||||
save.player.coins = math.max(0, math.min(save.player.coins or 0, Save.MAX_COINS))
|
||||
@@ -753,21 +762,24 @@ end
|
||||
-- copy is the witness that survives a crash mid-replace.
|
||||
function Save.save(save)
|
||||
if type(save) ~= "table" then return false, "no save" end
|
||||
if (save.version or "gold") == "gold" then
|
||||
Save.normalize(save)
|
||||
local version = save.version
|
||||
do
|
||||
local ok, SaveData = pcall(require, "src.core.SaveData")
|
||||
if ok and SaveData.activeSlot and not SaveData.activeSlot("gold") then
|
||||
local id = SaveData.createSlot and SaveData.createSlot("gold")
|
||||
if id and SaveData.setActiveSlot then SaveData.setActiveSlot("gold", id) end
|
||||
if ok and SaveData.activeSlot and not SaveData.activeSlot(version) then
|
||||
local id = SaveData.createSlot and SaveData.createSlot(version)
|
||||
if id and SaveData.setActiveSlot then
|
||||
SaveData.setActiveSlot(version, id)
|
||||
end
|
||||
end
|
||||
end
|
||||
local main, backup, tmp = saveNames(save.version)
|
||||
local main, backup, tmp = saveNames(version)
|
||||
local f = fs()
|
||||
if not f then return false, "no filesystem" end
|
||||
-- saveNames may now return a saves/<version>/<slot>.lua path, and
|
||||
-- love.filesystem.write does not create missing parent directories.
|
||||
local dir = main:match("^(.*)/[^/]+$")
|
||||
if dir and f.createDirectory then f.createDirectory(dir) end
|
||||
Save.normalize(save)
|
||||
save.savedAt = os.time()
|
||||
local encoded = SaveSerializer.encode(save)
|
||||
if f.getInfo(main) then
|
||||
|
||||
@@ -249,10 +249,12 @@ function SwitchDiagnostics.probeAssets(version)
|
||||
end
|
||||
|
||||
-- Shallow listing so we can see if the extract tree exists at all.
|
||||
local roots = { "yellow", "blue", "gold", "assets", "yellow/assets/generated",
|
||||
local roots = { "yellow", "blue", "gold", "silver", "assets",
|
||||
"yellow/assets/generated",
|
||||
"yellow/assets/generated/sprites", "blue/assets/generated/sprites",
|
||||
"gold/assets/generated", "gold/assets/generated/sprites",
|
||||
"gold/data/generated" }
|
||||
"gold/data/generated", "silver/assets/generated",
|
||||
"silver/data/generated" }
|
||||
for _, dir in ipairs(roots) do
|
||||
local info = filesystem.getInfo(dir)
|
||||
if info and info.type == "directory" and filesystem.getDirectoryItems then
|
||||
|
||||
@@ -27,7 +27,8 @@ local ok, err = pcall(function()
|
||||
CacheFs.prefix = prefix
|
||||
|
||||
local manifest = require("src.import.RomManifest").decode(version)
|
||||
local RomExtractor = version == "gold"
|
||||
local RomExtractor =
|
||||
require("src.core.GameVersion").generation(version) == 2
|
||||
and require("src.import.RomExtractorGen2")
|
||||
or require("src.import.RomExtractor")
|
||||
|
||||
|
||||
@@ -550,9 +550,10 @@ end
|
||||
-- opened on the Gold tab used to offer a dozen controls that did nothing
|
||||
-- and hide the seven that the cart itself has.
|
||||
--
|
||||
-- The block lives in options.lua under `gold`, which is exactly where
|
||||
-- src/core/gen2/Save.lua loadOptions reads it, so an edit here is live on the
|
||||
-- next boot the same way a Gen 1 edit is. Ladders mirror
|
||||
-- The block lives in options.lua under `gold` (the historical key; Gold and
|
||||
-- Silver share it the way the Gen 1 games share the flat namespace), which is
|
||||
-- exactly where src/core/gen2/Save.lua loadOptions reads it, so an edit here
|
||||
-- is live on the next boot the same way a Gen 1 edit is. Ladders mirror
|
||||
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
|
||||
local GEN2_KEY = "gold"
|
||||
|
||||
@@ -699,7 +700,9 @@ end
|
||||
function LauncherSettings.open(hooks, version)
|
||||
local opts = SaveData.loadOptions()
|
||||
local sections
|
||||
if version == "gold" then
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
if GameVersion.VERSIONS[version]
|
||||
and GameVersion.generation(version) == 2 then
|
||||
local block = opts[GEN2_KEY]
|
||||
if type(block) ~= "table" then
|
||||
block = {}
|
||||
|
||||
+128
-179
@@ -14,9 +14,9 @@
|
||||
-- height, and flex-shrink compressing text until it overlapped.
|
||||
--
|
||||
-- THE RULES THIS FILE FOLLOWS:
|
||||
-- * Lists paginate (Kit.pager). The installed-mod list also scrolls inside
|
||||
-- its viewport, so each of its pages can hold at least ten entries without
|
||||
-- requiring a tall window. Pages still bound how many mod rows we visit.
|
||||
-- * Short lists paginate (Kit.pager, perPage from Kit.rowsThatFit); the
|
||||
-- installed-mod list is one continuous scroll instead, drawing only the
|
||||
-- rows inside the region viewport so the window bounds the frame cost.
|
||||
-- * Every click handler only QUEUES work (imp._uiActions); update() drains
|
||||
-- the queue, so an action that tears the view down (Play, Edit save)
|
||||
-- never runs inside the frame that dispatched it.
|
||||
@@ -45,9 +45,6 @@ local COMMUNITY_URL = "https://bois.icu"
|
||||
local ACT_DEDUP = 0.35
|
||||
-- Finger travel past this (px) is a drag, not a tap.
|
||||
local TAP_SLOP2 = 16 * 16
|
||||
-- Installed mods should not turn into a one- or two-item pager on a compact
|
||||
-- display. Keep a useful page size, then let the list viewport scroll.
|
||||
local MIN_MODS_PER_PAGE = 10
|
||||
local MIN_SKIN_ROWS = 4
|
||||
local SKIN_FORMAT_LABEL = {
|
||||
native = "GEN1",
|
||||
@@ -81,15 +78,6 @@ local function setTabScroll(imp, value)
|
||||
imp._tabScroll[tabKeyOf(imp)] = clamp(value, 0, tabScrollMax(imp))
|
||||
end
|
||||
|
||||
local function modListWantsWheel(imp, wheel)
|
||||
if imp.tab ~= "mods" or (imp._modScrollMax or 0) <= 0 then return false end
|
||||
if not inRect(imp._modListRect, Kit.mouseX, Kit.mouseY) then return false end
|
||||
if not inRect(imp._tabRegionRect, Kit.mouseX, Kit.mouseY) then return false end
|
||||
local at = clamp(imp.modScroll or 0, 0, imp._modScrollMax)
|
||||
if wheel < 0 then return at < imp._modScrollMax end
|
||||
return at > 0
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- lifecycle
|
||||
|
||||
local function ensureState(imp)
|
||||
@@ -192,9 +180,6 @@ function LauncherView.touchpressed(imp, id, x, y)
|
||||
imp._touchAt = imp._touchAt or {}
|
||||
imp._touchAt[tostring(id)] = {
|
||||
x = x, y = y,
|
||||
modsList = imp.tab == "mods" and (imp._modScrollMax or 0) > 0
|
||||
and inRect(imp._modListRect, x, y)
|
||||
and inRect(imp._tabRegionRect, x, y),
|
||||
region = tabScrollMax(imp) > 0 and inRect(imp._tabRegionRect, x, y),
|
||||
}
|
||||
end
|
||||
@@ -207,19 +192,9 @@ function LauncherView.touchmoved(imp, id, x, y)
|
||||
if ddx * ddx + ddy * ddy > TAP_SLOP2 then
|
||||
start.dragged = true
|
||||
end
|
||||
-- A drag that began in the installed-mod viewport scrolls that page's
|
||||
-- rows. Its pager remains available for moving to the next ten-plus
|
||||
-- entries; a drag elsewhere keeps the normal short-window page scroll.
|
||||
if start.dragged then
|
||||
local last = start.lastY or start.y
|
||||
local move = -(y - last)
|
||||
if start.modsList then
|
||||
local listMax = imp._modScrollMax or 0
|
||||
local at, leftover = Kit.scrollHandoff(
|
||||
clamp(imp.modScroll or 0, 0, listMax), listMax, move)
|
||||
imp.modScroll = at
|
||||
move = leftover
|
||||
end
|
||||
if move ~= 0 and start.region then
|
||||
local at, leftover = Kit.scrollHandoff(tabScrollAt(imp),
|
||||
tabScrollMax(imp), move)
|
||||
@@ -343,7 +318,7 @@ end
|
||||
|
||||
local CART_COLOR = {
|
||||
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
|
||||
gold = PAL.railAmber,
|
||||
gold = PAL.railAmber, silver = PAL.railSilver,
|
||||
}
|
||||
local function cartColor(version)
|
||||
return CART_COLOR[version] or PAL.green
|
||||
@@ -942,6 +917,8 @@ local GAME_TABS = {
|
||||
label = "Yellow" },
|
||||
{ id = "gold", key = "tab-gold", letter = "G", color = PAL.railAmber,
|
||||
label = "Gold" },
|
||||
{ id = "silver", key = "tab-silver", letter = "S", color = PAL.railSilver,
|
||||
label = "Silver" },
|
||||
}
|
||||
|
||||
local HEADER_TABS = {
|
||||
@@ -1114,16 +1091,11 @@ local function buildHeader(imp, m)
|
||||
local tabGap = math.floor(6 * m.s)
|
||||
local tabRowGap = math.floor(4 * m.s)
|
||||
|
||||
-- the cartridge dropdown, sized to its longest label so switching games
|
||||
-- never reflows the row
|
||||
-- the cartridge dropdown: just the game's initial and the caret; the
|
||||
-- popup list carries the full names
|
||||
local chrome0 = headerChrome(imp)
|
||||
local game = currentGame(imp)
|
||||
local labelW = 0
|
||||
for _, g in ipairs(GAME_TABS) do
|
||||
labelW = math.max(labelW, Kit.textWidth("tab", Strings(g.label)))
|
||||
end
|
||||
local dropW = math.min(tabRight - tabLeft,
|
||||
tabH + labelW + math.floor(34 * m.s))
|
||||
local dropW = math.min(tabRight - tabLeft, tabH + math.floor(24 * m.s))
|
||||
chrome0.game.color = game.color
|
||||
chrome0.game.letter = game.letter
|
||||
chrome0.game.active = imp.tab == game.id
|
||||
@@ -1133,7 +1105,7 @@ local function buildHeader(imp, m)
|
||||
-- flip with it or it vanishes into the cartridge colour
|
||||
local gameInvert = chrome0.game.active or gameHot
|
||||
chrome0.game.ring = gameHot and not chrome0.game.active or nil
|
||||
btn(imp, tx, ty, dropW, tabH, "tab-game", Strings(game.label), chrome0.game)
|
||||
btn(imp, tx, ty, dropW, tabH, "tab-game", "", chrome0.game)
|
||||
do
|
||||
local cw = math.floor(7 * m.s)
|
||||
local ccx = tx + dropW - math.floor(14 * m.s)
|
||||
@@ -1964,7 +1936,6 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
cy = cy + buildModScopeRow(imp, x, cy, w, m)
|
||||
|
||||
if #mods == 0 then
|
||||
imp.modScroll, imp._modScrollMax, imp._modListRect = 0, 0, nil
|
||||
Kit.emptyBox(x, cy, w, math.floor(110 * m.s), imp:_modsEmptyHint())
|
||||
return (cy - y) + math.floor(110 * m.s)
|
||||
end
|
||||
@@ -2005,159 +1976,139 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
end
|
||||
|
||||
-- A mod row is a fixed height: its details first, then a dedicated second
|
||||
-- line of per-game checkboxes. Fixed because a page of uniform rows is
|
||||
-- what lets perPage come from the viewport.
|
||||
-- line of per-game checkboxes. Fixed row heights are what make the
|
||||
-- cull below plain arithmetic.
|
||||
local togH = math.floor(26 * m.s)
|
||||
local gamesLabel = Strings("Enable for:")
|
||||
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
+ Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small")
|
||||
local rowH = math.floor(8 * m.s) + textH + math.floor(8 * m.s) + togH
|
||||
+ math.floor(8 * m.s)
|
||||
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||
local listH = availH - (cy - y) - pagerH - gap
|
||||
local perPage = Kit.rowsThatFit(listH, rowH, gap, MIN_MODS_PER_PAGE, 20)
|
||||
local first, last, cur, pages = Kit.pageBounds(page(imp, "mods"), #mods, perPage)
|
||||
setPage(imp, "mods", cur)
|
||||
local listTop = cy
|
||||
local shown = math.max(0, last - first + 1)
|
||||
local contentH = shown * rowH + math.max(0, shown - 1) * gap
|
||||
local scrollMax = math.max(0, contentH - listH)
|
||||
local scroll = clamp(imp.modScroll or 0, 0, scrollMax)
|
||||
local lr = imp._modListRect
|
||||
if not lr then lr = {}; imp._modListRect = lr end
|
||||
lr.x, lr.y, lr.w, lr.h = x, listTop, w, listH
|
||||
imp._modScrollMax = scrollMax
|
||||
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks
|
||||
and Kit.hit(x, listTop, w, listH) then
|
||||
scroll = clamp(scroll - Kit.wheelY * math.floor(48 * m.s), 0, scrollMax)
|
||||
Kit.wheelY = 0
|
||||
elseif scrollMax == 0 then
|
||||
local wheelPage = Kit.wheelPage(x, listTop, w, listH, cur, #mods, perPage)
|
||||
if wheelPage ~= cur then imp.modScroll = 0 end
|
||||
setPage(imp, "mods", wheelPage)
|
||||
end
|
||||
imp.modScroll = scroll
|
||||
|
||||
Kit.pushClip(x, listTop, w, listH)
|
||||
for i = first, last do
|
||||
-- One continuous list: every row is laid out, the region scroll moves
|
||||
-- through all of it, and only rows inside the region's viewport draw --
|
||||
-- so the per-frame cost stays bounded by the window, not the list.
|
||||
local view = imp._tabRegionRect
|
||||
local viewTop = view and view.y or listTop
|
||||
local viewBot = view and (view.y + view.h) or (listTop + availH)
|
||||
for i = 1, #mods do
|
||||
local mod = mods[i]
|
||||
local ry = listTop + (i - first) * (rowH + gap) - scroll
|
||||
local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
|
||||
local isFullyDisabled = true
|
||||
if mod.enabledByVersion then
|
||||
for _, on in pairs(mod.enabledByVersion) do
|
||||
if on then isFullyDisabled = false; break end
|
||||
local ry = listTop + (i - 1) * (rowH + gap)
|
||||
if ry + rowH >= viewTop and ry <= viewBot then
|
||||
local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
|
||||
local isFullyDisabled = true
|
||||
if mod.enabledByVersion then
|
||||
for _, on in pairs(mod.enabledByVersion) do
|
||||
if on then isFullyDisabled = false; break end
|
||||
end
|
||||
else
|
||||
isFullyDisabled = not mod.enabled
|
||||
end
|
||||
else
|
||||
isFullyDisabled = not mod.enabled
|
||||
end
|
||||
|
||||
local focused = Kit.focusable(rowKey, x, ry, w, rowH)
|
||||
local hot = focused or Kit.hover(x, ry, w, rowH)
|
||||
if isFullyDisabled then
|
||||
Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted")
|
||||
else
|
||||
Kit.card(x, ry, w, rowH, hot)
|
||||
end
|
||||
local pad = math.floor(12 * m.s)
|
||||
local px, inner = x + pad, w - 2 * pad
|
||||
local ly = ry + math.floor(10 * m.s)
|
||||
|
||||
local togGap = math.floor(5 * m.s) + 1
|
||||
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
||||
|
||||
-- These answer separate games, not a single shared install flag. The
|
||||
-- importer receives the game id so an experimental confirmation also
|
||||
-- applies only to the checkbox the player pressed.
|
||||
local flipped = false
|
||||
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
|
||||
Kit.text("micro", gamesLabel, px,
|
||||
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
|
||||
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
|
||||
for _, game in ipairs(GameVersion.ORDER) do
|
||||
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
||||
if modGameCheckbox(tx, gamesY, togH,
|
||||
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
||||
game, togKey, not safeMode) then
|
||||
local version = game
|
||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
||||
flipped = true
|
||||
local focused = Kit.focusable(rowKey, x, ry, w, rowH)
|
||||
local hot = focused or Kit.hover(x, ry, w, rowH)
|
||||
if isFullyDisabled then
|
||||
Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted")
|
||||
else
|
||||
Kit.card(x, ry, w, rowH, hot)
|
||||
end
|
||||
tx = tx + togH + togGap
|
||||
end
|
||||
-- The checkboxes sit inside the row's rect, so their press also passes the
|
||||
-- row hit test; `flipped` gates the row action to everywhere else.
|
||||
if not flipped
|
||||
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
||||
local id = mod.id
|
||||
queueAction(imp, rowKey, function() imp._modActions = id end)
|
||||
end
|
||||
local textW = inner
|
||||
local pad = math.floor(12 * m.s)
|
||||
local px, inner = x + pad, w - 2 * pad
|
||||
local ly = ry + math.floor(10 * m.s)
|
||||
|
||||
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
|
||||
-- the games the mod is for, beside its category: the same chip the
|
||||
-- in-game manager shows (src/mods/ModTargets.lua)
|
||||
local gamesW = mod.targets
|
||||
and Kit.textWidth("micro", mod.targets) + math.floor(12 * m.s) or 0
|
||||
local nameShown = Kit.ellipsize("button", mod.name,
|
||||
textW - badgeW - gamesW - math.floor(12 * m.s))
|
||||
local headingCol = isFullyDisabled and PAL.muted or PAL.heading
|
||||
Kit.text("button", nameShown, px, ly, headingCol)
|
||||
local tagX = px + Kit.textWidth("button", nameShown) + math.floor(8 * m.s)
|
||||
Kit.tag(tagX, ly, badgeW, Kit.textHeight("button"), mod.badge,
|
||||
mod.experimental and PAL.yellow or PAL.muted)
|
||||
if mod.targets then
|
||||
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
|
||||
Kit.textHeight("button"), mod.targets,
|
||||
mod.targetsHere == false and PAL.steel or PAL.blue)
|
||||
end
|
||||
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
local togGap = math.floor(5 * m.s) + 1
|
||||
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
||||
|
||||
-- version + status + update state
|
||||
local statusText, statusCol = modStatusColor(mod.status)
|
||||
local line = "v" .. tostring(mod.version or "?") .. " " .. statusText
|
||||
Kit.text("small", line, px, ly, statusCol)
|
||||
local lx = px + Kit.textWidth("small", line) + math.floor(12 * m.s)
|
||||
if imp:_modInfoPending(mod.id) then
|
||||
-- An inline spinner, because this row's release check is genuinely in
|
||||
-- flight -- the list stays usable while it resolves.
|
||||
Loader.dot(lx, ly, Kit.textHeight("small"))
|
||||
Kit.text("small", Strings("Checking..."),
|
||||
lx + Kit.textHeight("small") + math.floor(6 * m.s), ly, PAL.muted)
|
||||
elseif info and info.status == "available" then
|
||||
Kit.text("small", Strings("v%s available", tostring(info.latest)),
|
||||
lx, ly, PAL.yellow)
|
||||
elseif info and info.status == "current" then
|
||||
Kit.text("small", Strings("up to date"), lx, ly, PAL.muted)
|
||||
elseif info and info.status == "error" then
|
||||
Kit.text("small", Strings("check failed"), lx, ly, PAL.red)
|
||||
end
|
||||
ly = ly + Kit.textHeight("small") + math.floor(2 * m.s)
|
||||
|
||||
-- one line of description, or the download stats when we have them
|
||||
-- (download count in green so popularity reads at a glance)
|
||||
if info and info.downloads then
|
||||
local d = info.dates
|
||||
local dl = ModUpdate.downloadsLine(info.downloads.total)
|
||||
local dates = ModUpdate.datesLine(d and d.first, d and d.latest)
|
||||
local segs = {}
|
||||
if dl then segs[#segs + 1] = { dl, PAL.green } end
|
||||
if dates then
|
||||
segs[#segs + 1] = { (dl and " - " or "") .. dates, PAL.detail }
|
||||
-- These answer separate games, not a single shared install flag. The
|
||||
-- importer receives the game id so an experimental confirmation also
|
||||
-- applies only to the checkbox the player pressed.
|
||||
local flipped = false
|
||||
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
|
||||
Kit.text("micro", gamesLabel, px,
|
||||
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
|
||||
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
|
||||
for _, game in ipairs(GameVersion.ORDER) do
|
||||
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
||||
if modGameCheckbox(tx, gamesY, togH,
|
||||
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
||||
game, togKey, not safeMode) then
|
||||
local version = game
|
||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
||||
flipped = true
|
||||
end
|
||||
tx = tx + togH + togGap
|
||||
end
|
||||
-- The checkboxes sit inside the row's rect, so their press also passes the
|
||||
-- row hit test; `flipped` gates the row action to everywhere else.
|
||||
if not flipped
|
||||
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
||||
local id = mod.id
|
||||
queueAction(imp, rowKey, function() imp._modActions = id end)
|
||||
end
|
||||
local textW = inner
|
||||
|
||||
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
|
||||
-- the games the mod is for, beside its category: the same chip the
|
||||
-- in-game manager shows (src/mods/ModTargets.lua)
|
||||
local gamesW = mod.targets
|
||||
and Kit.textWidth("micro", mod.targets) + math.floor(12 * m.s) or 0
|
||||
local nameShown = Kit.ellipsize("button", mod.name,
|
||||
textW - badgeW - gamesW - math.floor(12 * m.s))
|
||||
local headingCol = isFullyDisabled and PAL.muted or PAL.heading
|
||||
Kit.text("button", nameShown, px, ly, headingCol)
|
||||
local tagX = px + Kit.textWidth("button", nameShown) + math.floor(8 * m.s)
|
||||
Kit.tag(tagX, ly, badgeW, Kit.textHeight("button"), mod.badge,
|
||||
mod.experimental and PAL.yellow or PAL.muted)
|
||||
if mod.targets then
|
||||
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
|
||||
Kit.textHeight("button"), mod.targets,
|
||||
mod.targetsHere == false and PAL.steel or PAL.blue)
|
||||
end
|
||||
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
|
||||
-- version + status + update state
|
||||
local statusText, statusCol = modStatusColor(mod.status)
|
||||
local line = "v" .. tostring(mod.version or "?") .. " " .. statusText
|
||||
Kit.text("small", line, px, ly, statusCol)
|
||||
local lx = px + Kit.textWidth("small", line) + math.floor(12 * m.s)
|
||||
if imp:_modInfoPending(mod.id) then
|
||||
-- An inline spinner, because this row's release check is genuinely in
|
||||
-- flight -- the list stays usable while it resolves.
|
||||
Loader.dot(lx, ly, Kit.textHeight("small"))
|
||||
Kit.text("small", Strings("Checking..."),
|
||||
lx + Kit.textHeight("small") + math.floor(6 * m.s), ly, PAL.muted)
|
||||
elseif info and info.status == "available" then
|
||||
Kit.text("small", Strings("v%s available", tostring(info.latest)),
|
||||
lx, ly, PAL.yellow)
|
||||
elseif info and info.status == "current" then
|
||||
Kit.text("small", Strings("up to date"), lx, ly, PAL.muted)
|
||||
elseif info and info.status == "error" then
|
||||
Kit.text("small", Strings("check failed"), lx, ly, PAL.red)
|
||||
end
|
||||
ly = ly + Kit.textHeight("small") + math.floor(2 * m.s)
|
||||
|
||||
-- one line of description, or the download stats when we have them
|
||||
-- (download count in green so popularity reads at a glance)
|
||||
if info and info.downloads then
|
||||
local d = info.dates
|
||||
local dl = ModUpdate.downloadsLine(info.downloads.total)
|
||||
local dates = ModUpdate.datesLine(d and d.first, d and d.latest)
|
||||
local segs = {}
|
||||
if dl then segs[#segs + 1] = { dl, PAL.green } end
|
||||
if dates then
|
||||
segs[#segs + 1] = { (dl and " - " or "") .. dates, PAL.detail }
|
||||
end
|
||||
segLine("small", segs, px, ly, textW)
|
||||
elseif (mod.description or "") ~= "" then
|
||||
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
|
||||
px, ly, PAL.detail)
|
||||
end
|
||||
segLine("small", segs, px, ly, textW)
|
||||
elseif (mod.description or "") ~= "" then
|
||||
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
|
||||
px, ly, PAL.detail)
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
|
||||
local pagerY = listTop + listH + gap
|
||||
local newPage, newPagerH = Kit.pager(x, pagerY, w, cur, #mods, perPage, "mods")
|
||||
if newPage ~= cur then imp.modScroll = 0 end
|
||||
setPage(imp, "mods", newPage)
|
||||
return pagerY + newPagerH - y
|
||||
local contentH = #mods * rowH + (#mods - 1) * gap
|
||||
return (listTop + contentH + gap) - y
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------- find mods panel
|
||||
@@ -4851,8 +4802,7 @@ function LauncherView.draw(imp)
|
||||
Kit.blockClicks = modalUp(imp)
|
||||
|
||||
local step = Kit.scrollStep(m.s)
|
||||
local nested = modListWantsWheel(imp, Kit.wheelY or 0)
|
||||
if not nested then
|
||||
do
|
||||
local rect = imp._tabRegionRect
|
||||
if rect then
|
||||
setTabScroll(imp, (Kit.scrollWheel(tabScrollAt(imp), tabScrollMax(imp),
|
||||
@@ -4860,8 +4810,7 @@ function LauncherView.draw(imp)
|
||||
end
|
||||
end
|
||||
local scroll = math.max(0, math.min(imp._pageScroll or 0, scrollMax))
|
||||
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not nested
|
||||
and not Kit.blockClicks then
|
||||
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks then
|
||||
local moved = math.max(0, math.min(scroll - Kit.wheelY * step, scrollMax))
|
||||
if moved ~= scroll then
|
||||
scroll = moved
|
||||
@@ -4869,7 +4818,7 @@ function LauncherView.draw(imp)
|
||||
end
|
||||
end
|
||||
imp._pageScroll, imp._pageScrollMax = scroll, scrollMax
|
||||
if (Kit.wheelY or 0) ~= 0 and not nested and not Kit.blockClicks
|
||||
if (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks
|
||||
and tabScrollMax(imp) > 0 then
|
||||
local was = tabScrollAt(imp)
|
||||
local to = Kit.scrollClamp(was - Kit.wheelY * step, tabScrollMax(imp))
|
||||
|
||||
+122
-27
@@ -4,6 +4,7 @@
|
||||
-- (species order IS dex order, pics/tilesets are lz3-compressed rather than
|
||||
-- pkmncompress'd, maps are grouped instead of flat). See docs/gold-phase1.md.
|
||||
local bit = require("bit")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local ImageWriter = require("src.import.ImageWriter")
|
||||
local LuaWriter = require("src.import.LuaWriter")
|
||||
local Rom = require("src.import.Rom")
|
||||
@@ -168,6 +169,10 @@ function RomExtractorGen2.new(romData, manifest, progress)
|
||||
symbols = manifest.symbols,
|
||||
progress = progress,
|
||||
stage = 0,
|
||||
-- _GOLD / _SILVER: the labels are shared, the data behind a handful of
|
||||
-- them is not (gfx/misc.asm:9-20 vs :46-57).
|
||||
edition = GameVersion.forSha1(manifest.romSha1) == "silver"
|
||||
and "silver" or "gold",
|
||||
}, RomExtractorGen2)
|
||||
end
|
||||
|
||||
@@ -1641,22 +1646,40 @@ function RomExtractorGen2:extractTitle()
|
||||
return 3
|
||||
end
|
||||
|
||||
-- pret gfx/title/title_bg_gold.pal / title_fg.pal (5 BG pals, 2 OBJ pals).
|
||||
local BG_PALS = {
|
||||
local silver = self.edition == "silver"
|
||||
|
||||
-- pret gfx/title/title_bg_gold.pal / title_bg_silver.pal (5 BG pals);
|
||||
-- GSTitleBGPals is the edition-selected include (engine/gfx/color.asm:1234).
|
||||
local BG_PALS = silver and {
|
||||
{ { 31, 31, 31 }, { 0, 12, 15 }, { 4, 8, 21 }, { 0, 0, 0 } },
|
||||
{ { 31, 21, 0 }, { 15, 17, 15 }, { 4, 8, 21 }, { 0, 0, 17 } },
|
||||
{ { 31, 31, 31 }, { 31, 0, 0 }, { 4, 8, 21 }, { 0, 0, 0 } },
|
||||
{ { 31, 31, 31 }, { 24, 23, 25 }, { 4, 8, 21 }, { 8, 8, 9 } },
|
||||
{ { 31, 31, 31 }, { 5, 10, 11 }, { 0, 12, 15 }, { 0, 0, 0 } },
|
||||
} or {
|
||||
{ { 31, 31, 31 }, { 18, 23, 31 }, { 15, 20, 31 }, { 0, 0, 0 } },
|
||||
{ { 31, 21, 0 }, { 12, 14, 12 }, { 15, 20, 31 }, { 0, 0, 17 } },
|
||||
{ { 31, 31, 31 }, { 31, 0, 0 }, { 15, 20, 31 }, { 0, 0, 0 } },
|
||||
{ { 31, 31, 31 }, { 29, 25, 0 }, { 15, 20, 31 }, { 17, 10, 1 } },
|
||||
{ { 31, 31, 31 }, { 23, 26, 31 }, { 18, 23, 31 }, { 0, 0, 0 } },
|
||||
}
|
||||
-- title_fg.pal: pal 0 = Ho-Oh silhouette (shades 1-3 are the same brown);
|
||||
-- pal 1 = gold trail sparks (OAM_PAL1 on GSTitleTrail).
|
||||
-- title_fg.pal, shared (GSTitleOBPals, engine/gfx/color.asm:1241): pal 0 =
|
||||
-- Ho-Oh silhouette; pal 1 = gold trail sparks (OAM_PAL1 on GSTitleTrail).
|
||||
local OBJ_HOOH = {
|
||||
{ 31, 31, 31 }, { 7, 6, 3 }, { 7, 6, 3 }, { 7, 6, 3 },
|
||||
}
|
||||
local OBJ_TRAIL = {
|
||||
{ 31, 31, 31 }, { 31, 31, 0 }, { 26, 22, 0 }, { 0, 0, 0 },
|
||||
}
|
||||
-- engine/movie/title.asm:134-141 + CopyPals (home/palettes.asm:190):
|
||||
-- DmgToCgbObjPal0 %11100000 makes Silver's OBJ pal 0 {c0, c0, c2, c3}.
|
||||
if silver then
|
||||
OBJ_HOOH = {
|
||||
OBJ_HOOH[1], OBJ_HOOH[1], OBJ_HOOH[3], OBJ_HOOH[4],
|
||||
}
|
||||
-- .OAMData_GSTitleTrail is attribute 0, not OAM_PAL1 (oam.asm:834-837).
|
||||
OBJ_TRAIL = OBJ_HOOH
|
||||
end
|
||||
|
||||
local function palColor(pal, shade)
|
||||
local c = pal[shade + 1] or pal[4]
|
||||
@@ -1671,8 +1694,9 @@ function RomExtractorGen2:extractTitle()
|
||||
-- solid BLACK silhouette on a monochrome screen rather than the shaded pose
|
||||
-- a straight decode gives. rOBP1 (%11111000) carries the gold trail.
|
||||
local DMG_BGP = { 0, 2, 1, 3 }
|
||||
local DMG_OBP0 = { 3, 3, 3, 3 }
|
||||
local DMG_OBP1 = { 0, 2, 3, 3 }
|
||||
-- engine/movie/title.asm:105-115: Silver writes %11110000 to both OBPs.
|
||||
local DMG_OBP0 = silver and { 0, 0, 3, 3 } or { 3, 3, 3, 3 }
|
||||
local DMG_OBP1 = silver and { 0, 0, 3, 3 } or { 0, 2, 3, 3 }
|
||||
-- ImageWriter's four hardware shades, by shade number.
|
||||
local DMG_SHADE = { 1, 2 / 3, 1 / 3, 0 }
|
||||
|
||||
@@ -1776,6 +1800,28 @@ function RomExtractorGen2:extractTitle()
|
||||
|
||||
-- Ho-Oh frames from OAMData_GSIntroHoOh1..5 (data/sprite_anims/oam.asm).
|
||||
local hoohTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleScreenGFX4"), true)
|
||||
-- .OAMData_GSIntroLugia1 / 2 (data/sprite_anims/oam.asm:736-773); the
|
||||
-- spriteanimoam vtile offset is added per frame (core.asm:224-227).
|
||||
local LUGIA_1 = {
|
||||
{ -5, -2, 0, 0, 0x00 }, { -5, 0, 0, 0, 0x02 },
|
||||
{ -4, -2, 0, 0, 0x04 }, { -4, 0, 0, 0, 0x06 },
|
||||
{ -3, -1, 0, 0, 0x08 }, { -2, -1, 0, 0, 0x0a },
|
||||
{ -1, -2, 0, 0, 0x0c }, { -1, 0, 0, 0, 0x0e },
|
||||
{ 0, -2, 0, 0, 0x10 }, { 0, 0, 0, 0, 0x12 },
|
||||
{ 1, -2, 0, 0, 0x14 }, { 1, 0, 0, 0, 0x16 },
|
||||
{ 2, -2, 0, 0, 0x18 }, { 2, 0, 0, 0, 0x1a },
|
||||
{ 3, -1, 0, 0, 0x1c }, { 4, -1, 0, 0, 0x1e },
|
||||
}
|
||||
local LUGIA_2 = {
|
||||
{ -5, -2, 0, 0, 0x00 }, { -5, 0, 0, 0, 0x02 },
|
||||
{ -4, -2, 0, 0, 0x04 }, { -4, 0, 0, 0, 0x06 },
|
||||
{ -3, -1, 0, 0, 0x08 }, { -2, -1, 0, 0, 0x0a },
|
||||
{ -1, -2, 0, 0, 0x0c }, { -1, 0, 0, 0, 0x0e },
|
||||
{ 0, -2, 0, 0, 0x10 }, { 0, 0, 0, 0, 0x12 },
|
||||
{ 1, -2, 0, 0, 0x14 }, { 1, 0, 0, 0, 0x16 },
|
||||
{ 2, -2, 0, 0, 0x18 }, { 2, 0, 0, 0, 0x1a },
|
||||
{ 3, -2, 0, 0, 0x1c }, { 4, -2, 0, 0, 0x1e },
|
||||
}
|
||||
local HOOH_FRAMES = {
|
||||
{ -- 1
|
||||
{ -4, -1, 0, 0, 0x00 }, { -3, -2, 0, 0, 0x02 }, { -3, 0, 0, 0, 0x04 },
|
||||
@@ -1823,22 +1869,36 @@ function RomExtractorGen2:extractTitle()
|
||||
{ 3, -2, 0, 0, 0x22 }, { 3, 0, 0, 0, 0x24 },
|
||||
},
|
||||
}
|
||||
-- Frameset_GSIntroHoOhLugia (Gold): 1,2,3,4,3,5 with these durations.
|
||||
local HOOH_SEQUENCE = {
|
||||
-- Silver's five oamsets, as {layout, vtile base} (oam.asm:103-107).
|
||||
local LUGIA_FRAMES = {
|
||||
{ LUGIA_1, 0x00 }, { LUGIA_1, 0x20 }, { LUGIA_2, 0x40 },
|
||||
{ LUGIA_2, 0x60 }, { LUGIA_1, 0x00 },
|
||||
}
|
||||
-- Frameset_GSIntroHoOhLugia (data/sprite_anims/framesets.asm:376-396):
|
||||
-- Gold 1,2,3,4,3,5; Silver 2,1,2,3,3,4,4,3,2 on a faster clock.
|
||||
local HOOH_SEQUENCE = silver and {
|
||||
{ 2, 3 }, { 1, 7 }, { 2, 7 }, { 3, 7 }, { 3, 7 },
|
||||
{ 4, 7 }, { 4, 7 }, { 3, 7 }, { 2, 3 },
|
||||
} or {
|
||||
{ 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 },
|
||||
}
|
||||
local hoohPaths, hoohGrayPaths = {}, {}
|
||||
local originX, originY = 32, 24
|
||||
for fi, oam in ipairs(HOOH_FRAMES) do
|
||||
-- Lugia1/2 span x tiles -5..4, four tiles wider than Ho-Oh's -4..3.
|
||||
local originX, originY = silver and 40 or 32, 24
|
||||
local poseW = silver and 80 or 64
|
||||
local frames = silver and LUGIA_FRAMES or HOOH_FRAMES
|
||||
for fi, entry in ipairs(frames) do
|
||||
local oam = silver and entry[1] or entry
|
||||
local base = silver and entry[2] or 0
|
||||
-- The pose starts EMPTY, not white: an OBJ's colour 0 is transparent
|
||||
-- wherever it falls, so a gap enclosed by the bird shows the sky through
|
||||
-- exactly like one outside it, and there is no matte to flood-fill.
|
||||
local pose = ImageWriter.blank(64, 64, 0, 0, 0, 0)
|
||||
local pose = ImageWriter.blank(poseW, 64, 0, 0, 0, 0)
|
||||
for _, spr in ipairs(oam) do
|
||||
local px = originX + spr[1] * 8 + spr[3]
|
||||
local py = originY + spr[2] * 8 + spr[4]
|
||||
blitSprite(pose, hoohTiles[spr[5] + 1], px, py)
|
||||
blitSprite(pose, hoohTiles[spr[5] + 2], px, py + 8)
|
||||
blitSprite(pose, hoohTiles[base + spr[5] + 1], px, py)
|
||||
blitSprite(pose, hoohTiles[base + spr[5] + 2], px, py + 8)
|
||||
end
|
||||
local tinted = colorize(pose, function() return OBJ_HOOH end)
|
||||
local rel = ("title/hooh_%d.png"):format(fi)
|
||||
@@ -1855,14 +1915,20 @@ function RomExtractorGen2:extractTitle()
|
||||
end
|
||||
self:tick("Title screen", 3, 5)
|
||||
|
||||
-- Trail: TitleScreenGFX3 is raw 2bpp (8 tiles); Gold OAM uses one 8x16
|
||||
-- on OAM_PAL1 (gold), not the Ho-Oh silhouette pal.
|
||||
-- Trail: TitleScreenGFX3 is raw 2bpp; Gold's OAM is one 8x16, Silver's two
|
||||
-- side by side, and only 4 of Silver's 8 copied tiles exist (title.asm:43).
|
||||
local trailSym = self:symbol("TitleScreenGFX3")
|
||||
local trailRaw = self.rom:bytes(trailSym.bank, trailSym.address, 8 * 16)
|
||||
local trailTileCount = silver and 4 or 8
|
||||
local trailRaw =
|
||||
self.rom:bytes(trailSym.bank, trailSym.address, trailTileCount * 16)
|
||||
local trailTiles = tilesFrom2bpp(trailRaw, true)
|
||||
local trail = ImageWriter.blank(8, 16, 0, 0, 0, 0)
|
||||
local trail = ImageWriter.blank(silver and 16 or 8, 16, 0, 0, 0, 0)
|
||||
blitSprite(trail, trailTiles[1], 0, 0)
|
||||
blitSprite(trail, trailTiles[2], 0, 8)
|
||||
if silver then
|
||||
blitSprite(trail, trailTiles[3], 8, 0)
|
||||
blitSprite(trail, trailTiles[4], 8, 8)
|
||||
end
|
||||
local trailTint = colorize(trail, function() return OBJ_TRAIL end)
|
||||
self:save(trailTint, "title/trail.png")
|
||||
self:save(throughRegister(trail, DMG_OBP1), "title/trail_gray.png")
|
||||
@@ -1908,8 +1974,11 @@ function RomExtractorGen2:extractTitle()
|
||||
cloudsGray = "assets/generated/title/clouds_gray.png",
|
||||
hoohFramesGray = hoohGrayPaths,
|
||||
trailGray = "assets/generated/title/trail_gray.png",
|
||||
-- Frameset_GSIntroHoOhLugia (Gold), frame index 1-based + duration frames.
|
||||
-- Frameset_GSIntroHoOhLugia, frame index 1-based + duration frames.
|
||||
hoohSequence = HOOH_SEQUENCE,
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
|
||||
hoohBobAmplitude = silver and 8 or 2,
|
||||
hoohBobStep = silver and -1 or 1,
|
||||
-- `depixel 12, 11` (engine/movie/title.asm). Two traps, and the port had
|
||||
-- fallen into both, which is what put Ho-Oh off-centre:
|
||||
-- * ldpixel's own comment calls its first tile argument the X one and is
|
||||
@@ -1919,19 +1988,45 @@ function RomExtractorGen2:extractTitle()
|
||||
-- the cursor two rows up on the box screen. So this is x 88, y 96.
|
||||
-- * those are OAM coordinates, which are biased; a drawn object sits at
|
||||
-- (x - 8, y - 16) on screen.
|
||||
-- The pose canvas holds its own origin at (32, 24), so the sheet's corner
|
||||
-- is (88 - 8 - 32, 96 - 16 - 24) -- and the bird's 64px width then lands
|
||||
-- centred on the screen, 48 to 112.
|
||||
hoohX = 48,
|
||||
hoohY = 56,
|
||||
-- The pose canvas holds its own origin, so the sheet's corner is
|
||||
-- (88 - 8 - originX, 96 - 16 - originY) -- and the pose's width then lands
|
||||
-- centred on the screen (Ho-Oh 48..112, Lugia 40..120).
|
||||
hoohX = 80 - originX,
|
||||
hoohY = 80 - originY,
|
||||
trail = "assets/generated/title/trail.png",
|
||||
copyright = "assets/generated/title/copyright.png",
|
||||
copyrightSplash = "assets/generated/title/copyright_splash.png",
|
||||
-- ScrollTitleScreenClouds: Gold decrements the cloud-band SCX every
|
||||
-- 8 vblanks, so the strip slides 1px right. Silver does the same
|
||||
-- decrement every frame.
|
||||
cloudScrollEvery = 8,
|
||||
-- ScrollTitleScreenClouds (engine/menus/intro_menu.asm:917-928): Gold
|
||||
-- decrements the cloud-band SCX every 8 vblanks, so the strip slides 1px
|
||||
-- right. Silver does the same decrement every frame.
|
||||
cloudScrollEvery = silver and 1 or 8,
|
||||
cloudY = 88,
|
||||
-- BG pal 0 colour 2: the sky the widescreen bands have to match.
|
||||
sky = {
|
||||
BG_PALS[1][3][1] / 31, BG_PALS[1][3][2] / 31, BG_PALS[1][3][3] / 31,
|
||||
},
|
||||
-- The fill under the cloud/wave band: Gold's cloud field is BG pal 0
|
||||
-- colour 0 (white), Silver's sea floor is colour 3 (black).
|
||||
below = silver and {
|
||||
BG_PALS[1][4][1] / 31, BG_PALS[1][4][2] / 31, BG_PALS[1][4][3] / 31,
|
||||
} or {
|
||||
BG_PALS[1][1][1] / 31, BG_PALS[1][1][2] / 31, BG_PALS[1][1][3] / 31,
|
||||
},
|
||||
-- UpdateTitleTrailSprite (engine/menus/intro_menu.asm:1069-1124). Silver's
|
||||
-- `depixel 15, 11, 4, 0` is OAM (88, 124), less the bias and the (-16, -8)
|
||||
-- corner .OAMData_GSTitleTrail draws from.
|
||||
trailMode = silver and "silver" or "gold",
|
||||
trailSpawns = silver and { { 72, 100 } } or {
|
||||
{ 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 },
|
||||
{ 120, 88 }, { 88, 88 },
|
||||
},
|
||||
trailSpawnEvery = 4,
|
||||
trailStepX = 4,
|
||||
trailStepY = silver and 0 or 1,
|
||||
-- AnimSeq_GSTitleTrail (functions.asm:784-813) with wIntroSceneTimer 0.
|
||||
trailBobAmplitude = silver and 3 or 2,
|
||||
trailPhaseStep = silver and 7 or 3,
|
||||
trailPhase = silver and 0 or nil,
|
||||
}
|
||||
self:write("title", data)
|
||||
return data
|
||||
|
||||
+13
-10
@@ -142,6 +142,8 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
|
||||
"assets/generated/audio/programs.bin",
|
||||
},
|
||||
}
|
||||
-- Same Gen 2 extract, so a Silver cache is complete when the same files exist.
|
||||
VERSION_REQUIRED_FILES_OVERRIDE.silver = VERSION_REQUIRED_FILES_OVERRIDE.gold
|
||||
|
||||
-- "Split-screen ROM selector" first-run palette (matches the FirstRun mockup):
|
||||
-- a dark neon arcade panel, one column per game.
|
||||
@@ -335,7 +337,7 @@ function RomImporter.syncAndroidShortcuts(activeVersion)
|
||||
return false
|
||||
end
|
||||
|
||||
local allVersions = { "red", "blue", "yellow", "gold" }
|
||||
local allVersions = GameVersion.ORDER
|
||||
local ready = {}
|
||||
local seen = {}
|
||||
|
||||
@@ -1361,11 +1363,10 @@ function RomImporter.new(onComplete, opts)
|
||||
saveNotice = {},
|
||||
-- MODS panel state (pass 3): mods is the cached LauncherMods.list() array
|
||||
-- (refreshed lazily on first draw and after any toggle/install/delete);
|
||||
-- modScroll is the current paged list's inner scroll offset (px, clamped
|
||||
-- in draw); modNotice is the last install/delete result { ok, text }.
|
||||
-- modNotice is the last install/delete result { ok, text }.
|
||||
-- requiredImportNotice stays inside the imported-files modal so validation
|
||||
-- failures are visible beside the file picker that caused them.
|
||||
mods = nil, modScroll = 0, modNotice = nil, issueNotice = nil,
|
||||
mods = nil, modNotice = nil, issueNotice = nil,
|
||||
requiredImportNotice = nil,
|
||||
-- Which game the MODS panel is answering for (a GameVersion id, nil =
|
||||
-- every game). Rows resolve their enable-state and their "runs here"
|
||||
@@ -1422,7 +1423,8 @@ function RomImporter.new(onComplete, opts)
|
||||
self.returning[version] =
|
||||
(not ready) and marker ~= nil and marker ~= markerFor(version)
|
||||
self.romName[version] = "pokemon_" .. info.id
|
||||
.. ((info.id == "yellow" or info.id == "gold") and ".gbc" or ".gb")
|
||||
.. ((info.id == "yellow" or GameVersion.generation(version) == 2)
|
||||
and ".gbc" or ".gb")
|
||||
end
|
||||
RomImporter.syncAndroidShortcuts()
|
||||
self:_applyLastVersionTab()
|
||||
@@ -1687,7 +1689,7 @@ function RomImporter:startData(data, displayName)
|
||||
end
|
||||
if not isAcceptedRomSize(#data) then
|
||||
self:setError(("Expected a 1 MiB Game Boy ROM (Red/Blue/Yellow) or a "
|
||||
.. "2 MiB Game Boy Color ROM (Gold); this file is %.2f MiB.")
|
||||
.. "2 MiB Game Boy Color ROM (Gold/Silver); this file is %.2f MiB.")
|
||||
:format(#data / 1024 / 1024))
|
||||
return
|
||||
end
|
||||
@@ -1695,7 +1697,8 @@ function RomImporter:startData(data, displayName)
|
||||
local version = GameVersion.forSha1(actualHash)
|
||||
if not version then
|
||||
self:setError(("Unsupported ROM (SHA-1 %s). This needs a clean US Pokemon "
|
||||
.. "Red, Blue, Yellow, or Gold dump; patched, trimmed or \"fixed\" dumps "
|
||||
.. "Red, Blue, Yellow, Gold, or Silver dump; patched, trimmed or "
|
||||
.. "\"fixed\" dumps "
|
||||
.. "(tagged [b] or [BF]) never verify."):format(actualHash))
|
||||
return
|
||||
end
|
||||
@@ -1772,7 +1775,7 @@ function RomImporter:_startExtractCoroutine(version, info, prefix, displayName)
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
CacheFs.prefix = prefix
|
||||
local manifest = require("src.import.RomManifest").decode(version)
|
||||
local RomExtractor = version == "gold"
|
||||
local RomExtractor = GameVersion.generation(version) == 2
|
||||
and require("src.import.RomExtractorGen2")
|
||||
or require("src.import.RomExtractor")
|
||||
local extractor = RomExtractor.new(self.romData, manifest,
|
||||
@@ -2757,7 +2760,8 @@ function RomImporter:resumeAfterOverlay()
|
||||
end
|
||||
|
||||
function RomImporter:_cycleTab(delta)
|
||||
local order = { "red", "blue", "yellow", "gold", "mods", "find", "skins", "bug" }
|
||||
local order = { "red", "blue", "yellow", "gold", "silver",
|
||||
"mods", "find", "skins", "bug" }
|
||||
local idx = 1
|
||||
for i, id in ipairs(order) do
|
||||
if id == self.tab then idx = i; break end
|
||||
@@ -3123,7 +3127,6 @@ function RomImporter:_switchTab(id)
|
||||
self.tab = id
|
||||
self._findSearchFocus = false
|
||||
self._skinUrlFocus = false
|
||||
self._modScrollMax, self._modListRect = 0, nil
|
||||
self:_disarmTextInput()
|
||||
-- the skins list is cheap and can change behind the launcher's back
|
||||
-- (an export, a hand-dropped folder), so re-read it on every visit
|
||||
|
||||
@@ -228,7 +228,10 @@ SaveConvert.mergeDefaults = mergeDefaults
|
||||
-- codec exists yet. Both directions answer with a plain message the
|
||||
-- launcher's save card renders as-is, instead of pushing a Gen 2 save
|
||||
-- table through Gen 1 offsets and surfacing a codec traceback.
|
||||
local GEN2_SAV_UNSUPPORTED = { gold = "Pokemon Gold" }
|
||||
local GEN2_SAV_UNSUPPORTED = {
|
||||
gold = "Pokemon Gold",
|
||||
silver = "Pokemon Silver",
|
||||
}
|
||||
|
||||
-- importSav(bytes, version, gameVersion) -> saveTable, err
|
||||
-- bytes: the raw 32768-byte SRAM string. Validates size and the main-data
|
||||
|
||||
@@ -5,7 +5,7 @@ SyncMods.REV = 1
|
||||
local function versions()
|
||||
local ok, GameVersion = pcall(require, "src.core.GameVersion")
|
||||
if ok and GameVersion and GameVersion.ORDER then return GameVersion.ORDER end
|
||||
return { "red", "blue", "yellow", "gold" }
|
||||
return { "red", "blue", "yellow", "gold", "silver" }
|
||||
end
|
||||
|
||||
local function defaultDeps()
|
||||
|
||||
@@ -73,7 +73,10 @@ function Editor.load(opts)
|
||||
}
|
||||
local optsTbl = SaveData.loadOptions()
|
||||
local applied = optsTbl
|
||||
if opts.version == "gold" then
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local gen2 = GameVersion.VERSIONS[opts.version]
|
||||
and GameVersion.generation(opts.version) == 2
|
||||
if gen2 then
|
||||
local gold = type(optsTbl.gold) == "table" and optsTbl.gold or {}
|
||||
applied = {
|
||||
touchControls = gold.touchControls,
|
||||
@@ -151,7 +154,9 @@ local function persist()
|
||||
skin = cfg.skin,
|
||||
layouts = cfg.layouts,
|
||||
}
|
||||
if Editor.version == "gold" then
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
if GameVersion.VERSIONS[Editor.version]
|
||||
and GameVersion.generation(Editor.version) == 2 then
|
||||
opts.gold = type(opts.gold) == "table" and opts.gold or {}
|
||||
opts.gold.touchControls = block
|
||||
else
|
||||
@@ -569,7 +574,7 @@ function Editor.new(game)
|
||||
local state = { game = game, isOpaque = true }
|
||||
Editor.hostPoll = true
|
||||
Editor.load({
|
||||
version = "gold",
|
||||
version = require("src.core.GameVersion").get(),
|
||||
hostPoll = true,
|
||||
onClose = function()
|
||||
Editor.hostPoll = false
|
||||
|
||||
+12
-5
@@ -189,11 +189,18 @@ defineString("CREDIT_END", "END")
|
||||
|
||||
-- STAFF and everything after it is a heading. Anything BELOW this id is a
|
||||
-- person as far as ParseCredits is concerned.
|
||||
Credits.STAFF = defineString("STAFF", {
|
||||
" #MON",
|
||||
" GOLD VERSION",
|
||||
" PORT STAFF",
|
||||
})
|
||||
-- Credits_Staff differs per edition, including the centring spaces
|
||||
-- (data/credits_strings.asm:54-60).
|
||||
Credits.STAFF = defineString("STAFF",
|
||||
require("src.core.GameVersion").get() == "silver" and {
|
||||
" #MON",
|
||||
" SILVER VERSION",
|
||||
" PORT STAFF",
|
||||
} or {
|
||||
" #MON",
|
||||
" GOLD VERSION",
|
||||
" PORT STAFF",
|
||||
})
|
||||
defineString("DIRECTOR", " DIRECTOR")
|
||||
defineString("PROGRAMMING", " PROGRAMMING")
|
||||
defineString("ENGINE_DESIGN", " ENGINE DESIGN")
|
||||
|
||||
@@ -147,8 +147,8 @@ function GoldSilverIntro.new(game, opts)
|
||||
-- the movie's full 2335 frames over an empty screen and reads exactly
|
||||
-- like "the intro does not work". Say so instead: the fix is a
|
||||
-- re-import, and nothing else in the boot chain will mention it.
|
||||
Logger.warn("gold intro: no intro.lua in the cache -- re-import Gold "
|
||||
.. "or the movie plays blank")
|
||||
Logger.warn("gen2 intro: no intro.lua in the cache -- re-import "
|
||||
.. "this version or the movie plays blank")
|
||||
end
|
||||
self.images = {}
|
||||
self.sheets = {}
|
||||
|
||||
@@ -56,7 +56,7 @@ function MainMenu.new(game, opts)
|
||||
|
||||
self.save = opts.save
|
||||
if self.save == nil and opts.hasSave ~= false then
|
||||
local loaded = Save.load("gold")
|
||||
local loaded = Save.load()
|
||||
self.save = loaded
|
||||
end
|
||||
self.hasSave = opts.hasSave
|
||||
|
||||
@@ -34,8 +34,14 @@ local NamePick = {}
|
||||
NamePick.__index = NamePick
|
||||
NamePick.isOpaque = true
|
||||
|
||||
-- data/player_names.asm PlayerNameArray, Gold's half of the IF.
|
||||
-- data/player_names.asm PlayerNameArray, one half of the IF per edition.
|
||||
local PRESETS = { "GOLD", "HIRO", "TAYLOR", "KARL" }
|
||||
local PRESETS_SILVER = { "SILVER", "KAMON", "OSCAR", "MAX" }
|
||||
|
||||
local function presetsFor()
|
||||
local silver = require("src.core.GameVersion").get() == "silver"
|
||||
return silver and PRESETS_SILVER or PRESETS
|
||||
end
|
||||
|
||||
-- menu_coords 0, 0, 10, TEXTBOX_Y - 1 (TEXTBOX_Y = 12).
|
||||
local BOX_X1, BOX_Y1, BOX_X2, BOX_Y2 = 0, 0, 10, 11
|
||||
@@ -60,7 +66,7 @@ function NamePick.new(game, opts)
|
||||
self.game = game
|
||||
self.onDone = opts.onDone
|
||||
self.items = { "NEW NAME" }
|
||||
for _, name in ipairs(opts.presets or PRESETS) do
|
||||
for _, name in ipairs(opts.presets or presetsFor()) do
|
||||
self.items[#self.items + 1] = name
|
||||
end
|
||||
-- `db 1 ; default option`: the cursor starts on NEW NAME, not on a preset.
|
||||
@@ -106,7 +112,7 @@ function NamePick:openNaming()
|
||||
if name and #name > 0 then
|
||||
self:choose(name)
|
||||
else
|
||||
self:choose(self.items[2] or "GOLD")
|
||||
self:choose(self.items[2] or presetsFor()[1])
|
||||
end
|
||||
end,
|
||||
})
|
||||
@@ -146,7 +152,7 @@ function NamePick:update(_dt)
|
||||
if self.fontOk then
|
||||
self:openNaming()
|
||||
else
|
||||
self:choose(self.items[2] or "GOLD")
|
||||
self:choose(self.items[2] or presetsFor()[1])
|
||||
end
|
||||
else
|
||||
-- A preset returns through MovePlayerPicLeft, so the pic walks back
|
||||
@@ -227,5 +233,7 @@ function NamePick:drawWidescreen(winW, winH)
|
||||
end
|
||||
|
||||
NamePick.PRESETS = PRESETS
|
||||
NamePick.PRESETS_SILVER = PRESETS_SILVER
|
||||
NamePick.presetsFor = presetsFor
|
||||
|
||||
return NamePick
|
||||
|
||||
@@ -72,8 +72,8 @@ end
|
||||
|
||||
-- Naming presets are boot config the same way Gen 1 reads them
|
||||
-- (field.boot.namePresets), so a total conversion that replaces the list once
|
||||
-- replaces it for both games; NamePick.PRESETS (data/player_names.asm
|
||||
-- PlayerNameArray) is Gold's fallback.
|
||||
-- replaces it for both games; NamePick.presetsFor (data/player_names.asm
|
||||
-- PlayerNameArray) is the running edition's fallback.
|
||||
local function namePresets(game, who, fallback)
|
||||
local boot = game and game.data and game.data.field
|
||||
and game.data.field.boot
|
||||
@@ -351,9 +351,9 @@ function OakSpeech:openNamePick(step)
|
||||
picColors = self.playerColors,
|
||||
presets = step.presets
|
||||
or namePresets(self.game, step.presetsWho or step.who or "player",
|
||||
step.presetsFallback or NamePick.PRESETS),
|
||||
step.presetsFallback or NamePick.presetsFor()),
|
||||
onDone = function(name)
|
||||
name = name or "GOLD"
|
||||
name = name or NamePick.presetsFor()[1]
|
||||
self.game.save.player.name = name
|
||||
self.game.stack:pop() -- NamePick
|
||||
self.busy = false
|
||||
|
||||
@@ -181,7 +181,7 @@ function PcMenu:beginChangeBox(index)
|
||||
self.saveTimer = 0
|
||||
self.saved = nil
|
||||
local existed = self.saveExists
|
||||
if existed == nil then existed = Save.exists("gold") end
|
||||
if existed == nil then existed = Save.exists() end
|
||||
self.existed = existed
|
||||
end
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ function SaveMenu.new(game, opts)
|
||||
self.onDone = opts.onDone
|
||||
self.writer = opts.writer or Save.save
|
||||
local existed = opts.existed
|
||||
if existed == nil then existed = Save.exists("gold") end
|
||||
if existed == nil then existed = Save.exists() end
|
||||
self.existed = existed
|
||||
-- confirm -> overwrite (only when a file exists) -> saving -> done
|
||||
self.phase = "confirm"
|
||||
|
||||
+60
-25
@@ -2,6 +2,7 @@
|
||||
-- wing-flap (Frameset_GSIntroHoOhLugia), spark trails, A/Start to continue.
|
||||
-- drawWidescreen fills the window with sky/clouds so widescreen has no
|
||||
-- pillarbox voids; the 160x144 art stays aspect-centered on top.
|
||||
-- Every Gold/Silver difference arrives as a title.lua key, defaulted to Gold.
|
||||
|
||||
-- src/render/Assets.lua is the mod-override choke point: a raw
|
||||
-- love.graphics.newImage skips overrides/ and AssetTransform output.
|
||||
@@ -16,7 +17,7 @@ local TitleState = {}
|
||||
TitleState.__index = TitleState
|
||||
TitleState.isOpaque = true
|
||||
|
||||
-- title_bg_gold.pal mid-sky shade (sampled from composed title_screen.png).
|
||||
-- title_bg_gold.pal mid-sky shade, for a cache built before title.sky existed.
|
||||
local SKY = { 123 / 255, 165 / 255, 255 / 255, 1 }
|
||||
-- ...and its grey stand-in, for when COLOR is not GBC. The title art is the
|
||||
-- one thing in the port baked with its colours in (see the extractor), so the
|
||||
@@ -61,6 +62,15 @@ function TitleState.new(game, opts)
|
||||
self.hoohY = tonumber(title.hoohY) or 56
|
||||
self.cloudY = tonumber(title.cloudY) or 88
|
||||
self.cloudScrollEvery = tonumber(title.cloudScrollEvery) or 8
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
|
||||
self.hoohBobAmplitude = tonumber(title.hoohBobAmplitude) or 2
|
||||
self.hoohBobStep = tonumber(title.hoohBobStep) or 1
|
||||
local sky = title.sky
|
||||
self.sky = (type(sky) == "table" and #sky >= 3)
|
||||
and { sky[1], sky[2], sky[3], 1 } or SKY
|
||||
local below = title.below
|
||||
self.below = (type(below) == "table" and #below >= 3)
|
||||
and { below[1], below[2], below[3], 1 } or { 1, 1, 1, 1 }
|
||||
|
||||
self.hoohColor, self.hoohGray = {}, {}
|
||||
local paths = title.hoohFrames
|
||||
@@ -80,8 +90,12 @@ function TitleState.new(game, opts)
|
||||
self.sequence = title.hoohSequence or {
|
||||
{ 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 },
|
||||
}
|
||||
-- A frame shows duration + 1 ticks: GetSpriteAnimFrame stores the byte on
|
||||
-- the advancing tick and only decrements on the ones after
|
||||
-- (engine/sprite_anims/core.asm:400-434). Both editions' framesets total
|
||||
-- 64 ticks with it, locking the wing beat to the 64-tick sine bob.
|
||||
self.seqIndex = 1
|
||||
self.seqLeft = self.sequence[1] and self.sequence[1][2] or 10
|
||||
self.seqLeft = (self.sequence[1] and self.sequence[1][2] or 10) + 1
|
||||
self.frame = 1
|
||||
|
||||
-- AnimSeq_GSIntroHoOhLugia's SPRITEANIMSTRUCT_VAR1.
|
||||
@@ -89,12 +103,21 @@ function TitleState.new(game, opts)
|
||||
self.frameCounter = 0
|
||||
self.cloudScroll = 0
|
||||
self.trails = {}
|
||||
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm), in pixels.
|
||||
self.trailSpawns = {
|
||||
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm:1069-1124), in
|
||||
-- pixels.
|
||||
self.trailSpawns = title.trailSpawns or {
|
||||
{ 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 },
|
||||
{ 120, 88 }, { 88, 88 },
|
||||
}
|
||||
self.trailSpawnIndex = 1
|
||||
-- AnimSeq_GSTitleTrail (engine/sprite_anims/functions.asm:720-818).
|
||||
self.trailMode = title.trailMode or "gold"
|
||||
self.trailSpawnEvery = tonumber(title.trailSpawnEvery) or 4
|
||||
self.trailStepX = tonumber(title.trailStepX) or 4
|
||||
self.trailStepY = tonumber(title.trailStepY) or 1
|
||||
self.trailBobAmplitude = tonumber(title.trailBobAmplitude) or 2
|
||||
self.trailPhaseStep = tonumber(title.trailPhaseStep) or 3
|
||||
self.trailPhase = tonumber(title.trailPhase)
|
||||
-- How far past the 160px frame trails may fly (GB pixels); set each draw.
|
||||
self.trailMaxX = 200
|
||||
self.musicStarted = false
|
||||
@@ -119,47 +142,56 @@ function TitleState:enter()
|
||||
end
|
||||
end
|
||||
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm): VAR1 counts up
|
||||
-- one per frame and the struct's Y offset becomes `d * sin(VAR1 * pi/32)` with
|
||||
-- d = 2 on Gold (Silver counts DOWN with d = 8). Sprites_Sine hands back the
|
||||
-- byte the ASM leaves in a, so the down half of the wave arrives in two's
|
||||
-- complement and has to be read as a signed pixel delta here.
|
||||
function TitleState:hoohBob()
|
||||
local value = SpriteAnims.sine(self.hoohPhase, 2)
|
||||
if value >= 0x80 then value = value - 0x100 end
|
||||
-- Sprites_Sine hands back the byte the ASM leaves in a, so the down half of
|
||||
-- the wave arrives in two's complement and is a signed pixel delta here.
|
||||
local function signed(value)
|
||||
if value >= 0x80 then return value - 0x100 end
|
||||
return value
|
||||
end
|
||||
|
||||
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
|
||||
function TitleState:hoohBob()
|
||||
return signed(SpriteAnims.sine(self.hoohPhase, self.hoohBobAmplitude))
|
||||
end
|
||||
|
||||
function TitleState:advanceHooh()
|
||||
self.hoohPhase = (self.hoohPhase + 1) % 256
|
||||
self.hoohPhase = (self.hoohPhase + self.hoohBobStep) % 256
|
||||
self.seqLeft = self.seqLeft - 1
|
||||
if self.seqLeft > 0 then return end
|
||||
self.seqIndex = self.seqIndex + 1
|
||||
if self.seqIndex > #self.sequence then self.seqIndex = 1 end
|
||||
local step = self.sequence[self.seqIndex]
|
||||
self.frame = step[1]
|
||||
self.seqLeft = step[2]
|
||||
self.seqLeft = step[2] + 1
|
||||
end
|
||||
|
||||
function TitleState:spawnTrail()
|
||||
if not (self.trailColor or self.trailGray) then return end
|
||||
if self.frameCounter % 4 ~= 0 then return end
|
||||
if #self.trailSpawns == 0 then return end
|
||||
if self.frameCounter % self.trailSpawnEvery ~= 0 then return end
|
||||
local spawn = self.trailSpawns[self.trailSpawnIndex]
|
||||
self.trailSpawnIndex = self.trailSpawnIndex % #self.trailSpawns + 1
|
||||
if not spawn then return end
|
||||
self.trails[#self.trails + 1] = {
|
||||
x = spawn[1], y = spawn[2], phase = love.math.random(0, 255),
|
||||
x = spawn[1], y = spawn[2],
|
||||
phase = self.trailPhase or love.math.random(0, 255),
|
||||
}
|
||||
end
|
||||
|
||||
function TitleState:stepTrails()
|
||||
local alive = {}
|
||||
local maxX = self.trailMaxX or 200
|
||||
local silver = self.trailMode == "silver"
|
||||
for _, t in ipairs(self.trails) do
|
||||
t.x = t.x + 4
|
||||
t.y = t.y + 1
|
||||
t.phase = t.phase + 3
|
||||
t.drawY = t.y + math.floor(math.sin(t.phase / 16) * 2)
|
||||
t.x = t.x + self.trailStepX
|
||||
t.y = t.y + self.trailStepY
|
||||
t.phase = t.phase + self.trailPhaseStep
|
||||
if silver then
|
||||
t.drawY = t.y + signed(SpriteAnims.sine(t.phase, self.trailBobAmplitude))
|
||||
else
|
||||
t.drawY = t.y
|
||||
+ math.floor(math.sin(t.phase / 16) * self.trailBobAmplitude)
|
||||
end
|
||||
if t.x < maxX then alive[#alive + 1] = t end
|
||||
end
|
||||
self.trails = alive
|
||||
@@ -254,14 +286,17 @@ function TitleState:drawWidescreen(winW, winH)
|
||||
-- Let trails fly into the side bands.
|
||||
self.trailMaxX = math.ceil((winW - ox) / scale) + 16
|
||||
|
||||
-- Sky above the cloud line, paper white below : edge to edge. The fill has
|
||||
-- to match whichever baked set is showing, or the surround would stay blue
|
||||
-- around a grey screen.
|
||||
local sky = self:gray() and SKY_GRAY or SKY
|
||||
-- Sky above the cloud line, title.below under it (Gold's white cloud
|
||||
-- field, Silver's black sea) : edge to edge. The fill has to match
|
||||
-- whichever baked set is showing, or the surround would stay blue around a
|
||||
-- grey screen.
|
||||
local sky = self:gray() and SKY_GRAY or self.sky
|
||||
G.setColor(sky[1], sky[2], sky[3], 1)
|
||||
G.rectangle("fill", 0, 0, winW, math.max(0, cloudTop))
|
||||
G.setColor(1, 1, 1, 1)
|
||||
local below = self.below
|
||||
G.setColor(below[1], below[2], below[3], 1)
|
||||
G.rectangle("fill", 0, cloudTop, winW, winH - cloudTop)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
|
||||
-- Clouds across the full window width, aligned to the GB cloud band.
|
||||
G.push()
|
||||
|
||||
@@ -64,6 +64,7 @@ local PAL = {
|
||||
railBlue = { 70, 150, 255 },
|
||||
railGold = { 255, 203, 5 }, -- Yellow cartridge (bright)
|
||||
railAmber = { 218, 145, 32 }, -- Gold cartridge (deeper metal)
|
||||
railSilver = { 190, 198, 210 }, -- Silver cartridge (cool light metal)
|
||||
}
|
||||
-- Semantic aliases kept so ported call sites read the same as before.
|
||||
PAL.cardBorder = PAL.line
|
||||
@@ -321,10 +322,12 @@ function Theme.meter(x, y, w, h, pct, c)
|
||||
end
|
||||
|
||||
-- The 4px version rail across the top of both windows: the only brand
|
||||
-- colour on screen (Red / Blue / Yellow / Gold cartridge colours).
|
||||
-- colour on screen (Red / Blue / Yellow / Gold / Silver cartridge colours).
|
||||
function Theme.versionRail(x, y, w, h)
|
||||
if not G then return end
|
||||
local bars = { PAL.railRed, PAL.railBlue, PAL.railGold, PAL.railAmber }
|
||||
local bars = {
|
||||
PAL.railRed, PAL.railBlue, PAL.railGold, PAL.railAmber, PAL.railSilver,
|
||||
}
|
||||
local seg = w / #bars
|
||||
for i, c in ipairs(bars) do
|
||||
Theme.fill(x + (i - 1) * seg, y, seg, h, c, 1)
|
||||
|
||||
+16
-10
@@ -1569,11 +1569,11 @@ function World:readVar(varId)
|
||||
end
|
||||
|
||||
-- Script_checkver: 0 for Gold, 1 for Silver (constants/misc_constants.asm
|
||||
-- GS_VERSION). Only the Goldenrod prize counters and a handful of gift mons
|
||||
-- read it, and the port has no Silver cache yet, so an unset version is Gold.
|
||||
-- GS_VERSION).
|
||||
function World:gsVersion()
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local save = self.game and self.game.save
|
||||
local version = (save and save.version) or "gold"
|
||||
local version = (save and save.version) or GameVersion.get()
|
||||
return version == "silver" and 1 or 0
|
||||
end
|
||||
|
||||
@@ -6633,8 +6633,9 @@ function World:nameRival(onDone)
|
||||
if save then
|
||||
save.rival = save.rival or {}
|
||||
-- NameRival ends on `ld hl, wRivalName / ld de, .DefaultName / call
|
||||
-- InitName`, and .DefaultName is "SILVER@" on Gold
|
||||
-- (engine/events/specials.asm:80-91). The default has to be written
|
||||
-- InitName`, and .DefaultName is "SILVER@" on Gold and "GOLD@" on
|
||||
-- Silver (engine/events/specials.asm:80-94). The default has to be
|
||||
-- written
|
||||
-- HERE rather than ridden in from the seed: wRivalName starts as
|
||||
-- InitializeNPCNames' "???" (src/core/gen2/Save.lua), which is what the
|
||||
-- pre-naming Cherrygrove battle prints.
|
||||
@@ -6646,7 +6647,7 @@ function World:nameRival(onDone)
|
||||
if name and name:gsub(" ", "") ~= "" then
|
||||
save.rival.name = name
|
||||
else
|
||||
save.rival.name = "SILVER"
|
||||
save.rival.name = self:gsVersion() == 1 and "GOLD" or "SILVER"
|
||||
end
|
||||
end
|
||||
if onDone then onDone(name) end
|
||||
@@ -10026,8 +10027,11 @@ function World:draw()
|
||||
G.clear(0.07, 0.05, 0.02, 1)
|
||||
|
||||
if not self.mapImage or not self.player then
|
||||
G.setColor(0.85, 0.57, 0.13, 1)
|
||||
G.printf("POKEMON GOLD", 0, math.floor(h * 0.38), w, "center")
|
||||
local silver = self:gsVersion() == 1
|
||||
if silver then G.setColor(0.74, 0.78, 0.83, 1)
|
||||
else G.setColor(0.85, 0.57, 0.13, 1) end
|
||||
G.printf(silver and "POKEMON SILVER" or "POKEMON GOLD",
|
||||
0, math.floor(h * 0.38), w, "center")
|
||||
G.setColor(0.92, 0.90, 0.82, 1)
|
||||
G.printf(self.status or "No map.",
|
||||
0, math.floor(h * 0.48), w, "center")
|
||||
@@ -10140,8 +10144,10 @@ function World:draw()
|
||||
-- The survey overlay is a developer aid, not part of the game: POKEPORT_DEV
|
||||
-- (or the F3 toggle) shows it, a normal boot does not.
|
||||
if self.showDebugHud then
|
||||
G.setColor(0.85, 0.57, 0.13, 1)
|
||||
G.printf("POKEMON GOLD", 0, 10, w, "center")
|
||||
local silver = self:gsVersion() == 1
|
||||
if silver then G.setColor(0.74, 0.78, 0.83, 1)
|
||||
else G.setColor(0.85, 0.57, 0.13, 1) end
|
||||
G.printf(silver and "POKEMON SILVER" or "POKEMON GOLD", 0, 10, w, "center")
|
||||
G.setColor(0.92, 0.90, 0.82, 1)
|
||||
local label = string.format("%s (%d,%d) %s · %s · zoom %s",
|
||||
self.map.id, p.cellX, p.cellY, p.facing,
|
||||
|
||||
Reference in New Issue
Block a user