mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 15:31:15 +02:00
Spider Car Unleashed
CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
This commit is contained in:
@@ -266,18 +266,6 @@ local function coreRows(opts, hooks)
|
||||
end)
|
||||
end
|
||||
|
||||
-- issue #136: GBC FX soft-bricks the mobile present shader; same gate as
|
||||
-- the in-game row.
|
||||
local okFx, GBCFX = pcall(require, "src.render.GBCFX")
|
||||
if okFx and GBCFX.isSupported() then
|
||||
add(Strings("GBC FX"),
|
||||
function() return GBCFX.levelLabel(opts.gbcfx or 0) end,
|
||||
function(dir)
|
||||
opts.gbcfx = wrapIndex((opts.gbcfx or 0) + dir, 5)
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
local okZ, Zoom = pcall(require, "src.render.Zoom")
|
||||
if okZ then
|
||||
add(Strings("ZOOM"),
|
||||
@@ -678,17 +666,6 @@ local function gen2Rows(opts, hooks)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Same #136 gate as the Gen 1 row and the in-game one.
|
||||
local okFx, GBCFX = pcall(require, "src.render.GBCFX")
|
||||
if okFx and GBCFX.isSupported() then
|
||||
add(Strings("GBC FX"),
|
||||
function() return GBCFX.levelLabel(opts.gbcfx or 0) end,
|
||||
function(dir)
|
||||
opts.gbcfx = wrapIndex((opts.gbcfx or 0) + dir, 5)
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
local okVm, VideoMode = pcall(require, "src.core.VideoMode")
|
||||
if okVm then
|
||||
add(Strings("VIDEO MODE"),
|
||||
@@ -709,6 +686,10 @@ local function gen2Rows(opts, hooks)
|
||||
end)
|
||||
end
|
||||
|
||||
-- BATTLE BG (#1709): the WHITE/BLACK pair Gold's battle screen honours.
|
||||
add(Strings("BATTLE BG"), ladder(opts, "battleBg",
|
||||
{ { "white", "WHITE" }, { "black", "BLACK" } }, "white"))
|
||||
|
||||
addTouchRows(rows, add, opts, hooks)
|
||||
|
||||
return rows
|
||||
|
||||
+324
-51
@@ -417,14 +417,27 @@ local function shellColor(hex)
|
||||
return { tonumber(r, 16), tonumber(g, 16), tonumber(b, 16) }
|
||||
end
|
||||
|
||||
-- The real Crystal shell is glitter-flecked translucent plastic over a foil
|
||||
-- label, so it is the one stock cart that ships with a finish.
|
||||
local STOCK_FINISH = { crystal = "sparkle+holo" }
|
||||
|
||||
local function finishFlags(name)
|
||||
name = tostring(name or "")
|
||||
return name:find("sparkle", 1, true) ~= nil, name:find("holo", 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function cartSkin(imp, version)
|
||||
local row = imp.activeCartRow and imp:activeCartRow(version) or nil
|
||||
if not row then
|
||||
local sparkle, holo = finishFlags(STOCK_FINISH[version])
|
||||
return { cacheKey = version, color = cartColor(version),
|
||||
sparkle = sparkle, holo = holo,
|
||||
labelPath = "assets/labels/" .. tostring(version) .. ".png" }
|
||||
end
|
||||
local sparkle, holo = finishFlags(row.finish)
|
||||
return { cacheKey = "cart:" .. tostring(row.id),
|
||||
color = shellColor(row.shell) or cartColor(version),
|
||||
sparkle = sparkle, holo = holo,
|
||||
name = row.title, cart = row, cartId = row.id }
|
||||
end
|
||||
|
||||
@@ -572,8 +585,56 @@ vec4 position(mat4 transform_projection, vec4 vertex_position) {
|
||||
#endif
|
||||
|
||||
#ifdef PIXEL
|
||||
// finish: 0 plain, 1 sparkle (glitter suspended in the shell), 2 holographic
|
||||
// label sweep. Both are ours, not ported from anywhere.
|
||||
extern float finish;
|
||||
extern float finish_time;
|
||||
extern float finish_spin;
|
||||
|
||||
float sparkHash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(41.7321, 289.113))) * 43758.5453);
|
||||
}
|
||||
|
||||
// Flecks live on a jittered lattice so they read as suspended grains rather
|
||||
// than a regular grid, and each one twinkles on its own phase.
|
||||
vec3 sparkle(vec2 screen_coords) {
|
||||
vec2 cell = screen_coords / 19.0;
|
||||
vec2 id = floor(cell);
|
||||
vec2 f = fract(cell);
|
||||
float peak = 0.0;
|
||||
for (int oy = -1; oy <= 1; oy++) {
|
||||
for (int ox = -1; ox <= 1; ox++) {
|
||||
vec2 n = vec2(float(ox), float(oy));
|
||||
vec2 h = vec2(sparkHash(id + n), sparkHash(id + n + 17.0));
|
||||
float phase = sparkHash(id + n + 71.0) * 6.2831;
|
||||
float tw = sin(finish_time * 2.6 + phase + finish_spin * 3.0) * 0.5 + 0.5;
|
||||
float d = length(f - (n + h));
|
||||
peak = max(peak, smoothstep(0.13, 0.0, d) * pow(tw, 16.0));
|
||||
}
|
||||
}
|
||||
return vec3(peak);
|
||||
}
|
||||
|
||||
// Angle-dependent spectral sweep: a diagonal band whose hue walks with view
|
||||
// angle, so tilting the cart moves the rainbow the way real foil does.
|
||||
vec3 holo(vec2 uv) {
|
||||
float band = uv.x * 0.9 + uv.y * 0.6 + finish_spin * 0.55 + finish_time * 0.06;
|
||||
vec3 hue = 0.5 + 0.5 * cos(6.2831 * (band + vec3(0.0, 0.33, 0.67)));
|
||||
float interference = 0.5 + 0.5 * sin((uv.x - uv.y) * 62.0 + finish_time * 0.9);
|
||||
return hue * (0.55 + 0.45 * interference);
|
||||
}
|
||||
|
||||
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) {
|
||||
return Texel(tex, texture_coords) * color;
|
||||
vec4 px = Texel(tex, texture_coords) * color;
|
||||
if (finish > 1.5) {
|
||||
vec3 sheen = holo(texture_coords);
|
||||
float lum = dot(px.rgb, vec3(0.299, 0.587, 0.114));
|
||||
px.rgb = mix(px.rgb, px.rgb * (0.65 + sheen), 0.30 * (0.35 + 0.65 * lum));
|
||||
px.rgb += sheen * 0.05 * px.a;
|
||||
} else if (finish > 0.5) {
|
||||
px.rgb += sparkle(screen_coords) * 0.70 * px.a;
|
||||
}
|
||||
return px;
|
||||
}
|
||||
#endif
|
||||
]]
|
||||
@@ -601,6 +662,18 @@ local function cartSendHover(shader, mx, my, hovering, screenScale)
|
||||
return ok
|
||||
end
|
||||
|
||||
-- FINISH_* match the shader's `finish` uniform.
|
||||
local FINISH_NONE, FINISH_SPARKLE, FINISH_HOLO = 0, 1, 2
|
||||
|
||||
local function cartSendFinish(shader, mode, spin)
|
||||
if not shader or not shader.send then return end
|
||||
pcall(function()
|
||||
shader:send("finish", mode)
|
||||
shader:send("finish_time", Kit.time or 0)
|
||||
shader:send("finish_spin", spin or 0)
|
||||
end)
|
||||
end
|
||||
|
||||
local function cartridgeButton(imp, x, y, w, h, key, skin, gameName, action)
|
||||
local state = cartridgeState(imp, skin.cacheKey)
|
||||
markNoDrag(imp, x, y, w, h)
|
||||
@@ -723,6 +796,10 @@ local function cartridgeButton(imp, x, y, w, h, key, skin, gameName, action)
|
||||
love.graphics.rotate(juiceR * 2)
|
||||
love.graphics.translate(-cx, -cy)
|
||||
if useHover then love.graphics.setShader(shader) end
|
||||
if useHover then
|
||||
cartSendFinish(shader, skin.sparkle and FINISH_SPARKLE or FINISH_NONE,
|
||||
state.spin)
|
||||
end
|
||||
|
||||
local capH = h * 3 / 65
|
||||
local mainTop = -halfH + capH
|
||||
@@ -837,6 +914,9 @@ local function cartridgeButton(imp, x, y, w, h, key, skin, gameName, action)
|
||||
local labelPoints = cartQuad(project, labelX, labelY, labelW, labelH, faceZ + 1.2)
|
||||
local label = cartridgeLabel(imp, skin.cacheKey, skin.labelPath, skin.cartId)
|
||||
local mesh = label and cartLabelMesh(imp, skin.cacheKey, label, labelPoints)
|
||||
if useHover and skin.holo then
|
||||
cartSendFinish(shader, FINISH_HOLO, state.spin)
|
||||
end
|
||||
if mesh then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(mesh)
|
||||
@@ -845,6 +925,10 @@ local function cartridgeButton(imp, x, y, w, h, key, skin, gameName, action)
|
||||
love.graphics.draw(label.image, labelPoints[1][1], labelPoints[1][2],
|
||||
0, artScale, artScale)
|
||||
end
|
||||
if useHover and skin.holo then
|
||||
cartSendFinish(shader, skin.sparkle and FINISH_SPARKLE or FINISH_NONE,
|
||||
state.spin)
|
||||
end
|
||||
cartPolygon({
|
||||
{ project(-w * 0.07, h * 0.37, faceZ + 1) },
|
||||
{ project(w * 0.07, h * 0.37, faceZ + 1) },
|
||||
@@ -863,6 +947,8 @@ local function modStatusColor(status)
|
||||
if status == "safe_mode" then return Strings("Safe mode"), PAL.yellow end
|
||||
if status == "needs_import" then return Strings("Import required"), PAL.yellow end
|
||||
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
||||
-- a cart pins it, but nothing on this install provides it
|
||||
if status == "missing" then return Strings("Not installed"), PAL.red end
|
||||
-- not a fault: the mod is intact, this is simply not a game it is for
|
||||
-- (src/mods/ModTargets.lua)
|
||||
if status == "other_game" then return Strings("Not for this game"), PAL.muted end
|
||||
@@ -971,6 +1057,28 @@ local function buildModScopeRow(imp, x, y, w, m)
|
||||
return h + math.floor(8 * m.s)
|
||||
end
|
||||
|
||||
-- Says whose mod list is on screen when a cart owns it, and what its seal
|
||||
-- lets the player do with it.
|
||||
local function buildModCartRow(imp, x, y, w, m, cartId, report)
|
||||
if not cartId then return 0 end
|
||||
local title = (report and report.title) or cartId
|
||||
local seal = (report and report.seal) or "sealed"
|
||||
local line
|
||||
if seal == "sealed+" then
|
||||
line = Strings("%s pins these mods. You may switch any of them on or off, but not add or remove any.",
|
||||
title)
|
||||
elseif seal == "open" then
|
||||
line = Strings("%s pins these mods. Anything it ships switched off is yours to switch on.",
|
||||
title)
|
||||
else
|
||||
line = Strings("%s is sealed: these are its mods, and they run exactly as pinned. Break the seal on the cart's own page to change that.",
|
||||
title)
|
||||
end
|
||||
local h = Kit.textWrapped("small", line, x, y, w,
|
||||
seal == "sealed" and PAL.yellow or PAL.blue, 3)
|
||||
return h + math.floor(8 * m.s)
|
||||
end
|
||||
|
||||
local function buildSaveCartRow(imp, x, y, w, m)
|
||||
local version = imp.modScope
|
||||
local h = m.btnH
|
||||
@@ -1000,6 +1108,12 @@ local function buildSaveCartRow(imp, x, y, w, m)
|
||||
return h + gap
|
||||
end
|
||||
|
||||
-- The launcher's name for a game id, where all a row has is the id.
|
||||
local function gameLabel(version)
|
||||
local info = GameVersion.info(version)
|
||||
return (info and (info.launcherName or info.displayName)) or tostring(version)
|
||||
end
|
||||
|
||||
local function findActionFor(entry, installedVersion)
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
if not ModIndex.canInstall(entry) then
|
||||
@@ -1837,6 +1951,11 @@ local function SEAL_LABEL(armed)
|
||||
return armed and Strings("Break it") or Strings("Break the seal")
|
||||
end
|
||||
|
||||
local function FILL_LABEL(count)
|
||||
if count > 1 then return Strings("Install %d required mods", count) end
|
||||
return Strings("Install required mods")
|
||||
end
|
||||
|
||||
local function sealSlotName(slot)
|
||||
if not slot then return nil end
|
||||
if type(slot.label) == "string" and slot.label ~= "" then return slot.label end
|
||||
@@ -1849,16 +1968,25 @@ local function buildCartCard(imp, x, y, w, m, version)
|
||||
if not report then return 0 end
|
||||
local title = tostring(report.title or report.id or "")
|
||||
local broken = (slot and slot.sealBroken == true) or false
|
||||
local fillCount = imp.cartFillRows and #imp:cartFillRows(version) or 0
|
||||
local state, stateCol, body = nil, PAL.green, {}
|
||||
if report.refused then
|
||||
state, stateCol = Strings("This cart will not start"), PAL.red
|
||||
body[#body + 1] = { report.message, PAL.detail }
|
||||
if fillCount > 0 then
|
||||
body[#body + 1] = { Strings("Install the mods it pins to play it the way its author built it."),
|
||||
PAL.detail }
|
||||
end
|
||||
body[#body + 1] = { Strings("Break the seal to play it with the mods you have."),
|
||||
PAL.detail }
|
||||
elseif broken then
|
||||
state, stateCol = Strings("Seal broken"), PAL.yellow
|
||||
body[#body + 1] = { Strings("This save loads the cart's pinned mods first, then your other enabled mods. It is marked modified."),
|
||||
PAL.detail }
|
||||
elseif report.seal == "sealed+" then
|
||||
state = Strings("Sealed - ready to play")
|
||||
body[#body + 1] = { Strings("This cart loads only the mods it pins. You can switch any of them on or off."),
|
||||
PAL.detail }
|
||||
elseif report.sealed then
|
||||
state = Strings("Sealed - ready to play")
|
||||
body[#body + 1] = { Strings("This cart loads only the mods it pins."),
|
||||
@@ -1883,15 +2011,41 @@ local function buildCartCard(imp, x, y, w, m, version)
|
||||
PAL.yellow }
|
||||
body[#body + 1] = { Strings("Press Break it again to do it."), PAL.yellow }
|
||||
end
|
||||
-- What the last install run managed, and per mod what it could not.
|
||||
local fillNotice = imp.cartFillNotice
|
||||
if fillNotice then
|
||||
body[#body + 1] = { fillNotice.text,
|
||||
fillNotice.ok and PAL.green or PAL.red }
|
||||
for _, line in ipairs(fillNotice.failures or {}) do
|
||||
body[#body + 1] = { line, PAL.red }
|
||||
end
|
||||
end
|
||||
|
||||
local pad = math.floor(14 * m.s)
|
||||
local iw = w - 2 * pad
|
||||
local chipH = offer and math.max(Kit.tapMin(), math.floor(30 * m.s)) or 0
|
||||
local fillLabel = FILL_LABEL(fillCount)
|
||||
local chipGap = math.floor(8 * m.s)
|
||||
local chipH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||
local fillW = fillCount > 0
|
||||
and math.min(iw, chipWidth(fillLabel, m)) or 0
|
||||
local sealW = offer and math.min(iw, math.max(chipWidth(SEAL_LABEL(false), m),
|
||||
chipWidth(SEAL_LABEL(true), m))) or 0
|
||||
-- Both chips share a row when they fit; a narrow card stacks them instead.
|
||||
local sideBySide = fillW > 0 and sealW > 0
|
||||
and (fillW + chipGap + sealW) <= iw
|
||||
local chipRows = 0
|
||||
if fillW > 0 then chipRows = chipRows + 1 end
|
||||
if sealW > 0 then chipRows = chipRows + 1 end
|
||||
if sideBySide then chipRows = 1 end
|
||||
if chipRows == 0 then chipH = 0 end
|
||||
|
||||
local h = pad + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
for _, line in ipairs(body) do
|
||||
h = h + Kit.wrapHeight("small", line[1] or "", iw, 3)
|
||||
end
|
||||
if offer then h = h + math.floor(8 * m.s) + chipH end
|
||||
if chipRows > 0 then
|
||||
h = h + math.floor(8 * m.s) + chipRows * chipH + (chipRows - 1) * chipGap
|
||||
end
|
||||
h = h + pad
|
||||
|
||||
Kit.card(x, y, w, h)
|
||||
@@ -1902,14 +2056,23 @@ local function buildCartCard(imp, x, y, w, m, version)
|
||||
cy = cy + Kit.textWrapped("small", line[1] or "", x + pad, cy, iw,
|
||||
line[2], 3)
|
||||
end
|
||||
if offer then
|
||||
if chipRows > 0 then
|
||||
cy = cy + math.floor(8 * m.s)
|
||||
local cw = math.max(chipWidth(SEAL_LABEL(false), m),
|
||||
chipWidth(SEAL_LABEL(true), m))
|
||||
btn(imp, x + pad, cy, cw, chipH, "seal-" .. scope, SEAL_LABEL(armed), {
|
||||
kind = "danger", font = "small", keepArm = true,
|
||||
action = function() imp:pressBreakSeal(version) end,
|
||||
})
|
||||
local bx = x + pad
|
||||
if fillW > 0 then
|
||||
btn(imp, bx, cy, fillW, chipH, "cart-fill-" .. scope, fillLabel, {
|
||||
kind = "primary", font = "small",
|
||||
action = function() imp:pressInstallCartMods(version) end,
|
||||
})
|
||||
if sideBySide then bx = bx + fillW + chipGap
|
||||
else cy = cy + chipH + chipGap end
|
||||
end
|
||||
if sealW > 0 then
|
||||
btn(imp, bx, cy, sealW, chipH, "seal-" .. scope, SEAL_LABEL(armed), {
|
||||
kind = "danger", font = "small", keepArm = true,
|
||||
action = function() imp:pressBreakSeal(version) end,
|
||||
})
|
||||
end
|
||||
end
|
||||
return h
|
||||
end
|
||||
@@ -2182,6 +2345,10 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
local mods = imp.mods or {}
|
||||
local gap = m.gap
|
||||
local cy = y
|
||||
local cartId, cartReport
|
||||
if imp.modCartPlan then cartId, cartReport = imp:modCartPlan() end
|
||||
-- a cart owns its mod set: only the pins it already ships may be switched
|
||||
local bulkOk = not safeMode and cartId == nil
|
||||
|
||||
-- header: progressive action cluster. Surfaces primary/frequent actions
|
||||
-- (Import, Updates, Sort) directly on the bar across screen sizes, placing
|
||||
@@ -2210,11 +2377,11 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
action = function() imp:chooseMod() end })
|
||||
btn(imp, place(disableW), cy, disableW, bh, "mods-disable-all", Strings("Disable all"), {
|
||||
kind = "warn", font = "small",
|
||||
enabled = not safeMode,
|
||||
enabled = bulkOk,
|
||||
action = function() imp:_setAllMods(false) end })
|
||||
btn(imp, place(enableW), cy, enableW, bh, "mods-enable-all", Strings("Enable all"), {
|
||||
kind = "good", font = "small",
|
||||
enabled = not safeMode,
|
||||
enabled = bulkOk,
|
||||
action = function() imp:_setAllMods(true) end })
|
||||
btn(imp, place(checkFullW), cy, checkFullW, bh, "mods-check-updates", Strings("Check for updates"), {
|
||||
font = "small",
|
||||
@@ -2275,6 +2442,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
+ math.floor(8 * m.s)
|
||||
|
||||
cy = cy + buildModScopeRow(imp, x, cy, w, m)
|
||||
cy = cy + buildModCartRow(imp, x, cy, w, m, cartId, cartReport)
|
||||
cy = cy + buildSaveCartRow(imp, x, cy, w, m)
|
||||
|
||||
if #mods == 0 then
|
||||
@@ -2368,22 +2536,40 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
|
||||
-- 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.
|
||||
-- applies only to the checkbox the player pressed. A cart's pin answers
|
||||
-- one game -- the cart's -- so it gets one switch instead of the row.
|
||||
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)
|
||||
local rowLabel = gamesLabel
|
||||
if mod.cartPin then
|
||||
rowLabel = mod.cartTogglable and Strings("In this cart:")
|
||||
or Strings("Pinned, sealed:")
|
||||
end
|
||||
Kit.text("micro", rowLabel, px,
|
||||
gamesY + (togH - Kit.textHeight("micro")) / 2,
|
||||
mod.cartPin and not mod.cartTogglable and PAL.yellow or PAL.muted)
|
||||
local tx = px + Kit.textWidth("micro", rowLabel) + math.floor(10 * m.s)
|
||||
if mod.cartPin then
|
||||
local togKey = "mod-toggle-" .. mod.id .. "-cart"
|
||||
-- pressable even when the seal refuses it, so the panel can say why
|
||||
if modGameCheckbox(tx, gamesY, togH, mod.enabled == true,
|
||||
mod.cartBase or "red", togKey, not safeMode) then
|
||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, nil) end)
|
||||
flipped = true
|
||||
end
|
||||
tx = tx + togH + togGap
|
||||
else
|
||||
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
|
||||
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.
|
||||
@@ -2399,17 +2585,27 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
-- 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
|
||||
-- the cart's own list, not the player's: every row says so
|
||||
local pinLabel = mod.cartPin and Strings("PINNED") or nil
|
||||
local pinW = pinLabel
|
||||
and Kit.textWidth("micro", pinLabel) + math.floor(12 * m.s) or 0
|
||||
local nameShown = Kit.ellipsize("button", mod.name,
|
||||
textW - badgeW - gamesW - math.floor(12 * m.s))
|
||||
textW - badgeW - gamesW - pinW - 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)
|
||||
tagX = tagX + badgeW + math.floor(4 * m.s)
|
||||
if mod.targets then
|
||||
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
|
||||
Kit.tag(tagX, ly, gamesW,
|
||||
Kit.textHeight("button"), mod.targets,
|
||||
mod.targetsHere == false and PAL.steel or PAL.blue)
|
||||
tagX = tagX + gamesW + math.floor(4 * m.s)
|
||||
end
|
||||
if pinLabel then
|
||||
Kit.tag(tagX, ly, pinW, Kit.textHeight("button"), pinLabel,
|
||||
mod.cartTogglable and PAL.blue or PAL.yellow)
|
||||
end
|
||||
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
|
||||
@@ -2830,14 +3026,45 @@ local function buildBugPanel(imp, x, y, w, availH, m)
|
||||
end })
|
||||
end
|
||||
|
||||
-- FIND tab: which half of the feed is on screen, in the same chip idiom the
|
||||
-- MODS tab's scope row uses. The counts say which half is worth pressing.
|
||||
local function buildFindKindRow(imp, x, y, w, m)
|
||||
local h = math.max(Kit.tapMin(), math.floor(26 * m.s))
|
||||
local gap = math.floor(6 * m.s)
|
||||
local label = Strings("Browse:")
|
||||
Kit.text("small", label, x, y + (h - Kit.textHeight("small")) / 2, PAL.muted)
|
||||
local cx = x + Kit.textWidth("small", label) + math.floor(10 * m.s)
|
||||
local options = {
|
||||
{ id = "mods", label = Strings("Mods"),
|
||||
n = #((imp.findIndex and imp.findIndex.mods) or {}) },
|
||||
{ id = "carts", label = Strings("Carts"),
|
||||
n = #((imp.findIndex and imp.findIndex.carts) or {}) },
|
||||
}
|
||||
for _, opt in ipairs(options) do
|
||||
local text = ("%s (%d)"):format(opt.label, opt.n)
|
||||
local cw = Kit.textWidth("micro", text) + math.floor(18 * m.s)
|
||||
if cx + cw > x + w then break end
|
||||
if Kit.chip(cx, y, cw, h, text, imp.findKind == opt.id, PAL.lineStrong,
|
||||
"find-kind-" .. opt.id) then
|
||||
local want = opt.id
|
||||
queueAction(imp, "find-kind-" .. want,
|
||||
function() imp:_setFindKind(want) end)
|
||||
end
|
||||
cx = cx + cw + gap
|
||||
end
|
||||
return h + math.floor(8 * m.s)
|
||||
end
|
||||
|
||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
imp:_ensureFind()
|
||||
imp:_ensureMods()
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
local sources = imp.findSources or {}
|
||||
local carts = imp.findKind == "carts"
|
||||
local rows = imp:_findRows()
|
||||
local total = #((imp.findIndex and imp.findIndex.mods) or {})
|
||||
local total = #((imp.findIndex
|
||||
and (carts and imp.findIndex.carts or imp.findIndex.mods)) or {})
|
||||
local gap = m.gap
|
||||
local cy = y
|
||||
|
||||
@@ -2868,6 +3095,8 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
return (cy - y) + h
|
||||
end
|
||||
|
||||
cy = cy + buildFindKindRow(imp, x, cy, w, m)
|
||||
|
||||
-- One row: the search field, then Filter / Sort / Indexes popup buttons.
|
||||
local fieldH = math.max(Kit.tapMin(), math.floor(36 * m.s))
|
||||
local bgap = math.floor(6 * m.s)
|
||||
@@ -2880,22 +3109,31 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
btn(imp, place(sw), cy, sw, fieldH, "find-sort", Strings("Sort"), {
|
||||
font = "small",
|
||||
action = function() imp._sortPopup = "find" end })
|
||||
-- The Filter button carries its state: blue while a category is active,
|
||||
-- so a filtered-down list never reads as "the index shrank".
|
||||
-- The Filter button carries its state: blue while a category (mods) or a
|
||||
-- base game (carts) is active, so a filtered-down list never reads as "the
|
||||
-- index shrank".
|
||||
local activeFilter = carts and imp.findBase or imp.findCategory
|
||||
local fw = Kit.textWidth("small", Strings("Filter")) + math.floor(20 * m.s)
|
||||
btn(imp, place(fw), cy, fw, fieldH, "find-filter", Strings("Filter"), {
|
||||
kind = imp.findCategory and "accent" or "ghost", font = "small",
|
||||
kind = activeFilter and "accent" or "ghost", font = "small",
|
||||
action = function() imp._filterPopup = true end })
|
||||
local searchW = place(0) - x - bgap
|
||||
textField(imp, x, cy, searchW, fieldH, "find-search", imp.findQuery or "",
|
||||
Strings("Search mods"), imp._findSearchFocus == true,
|
||||
carts and Strings("Search carts") or Strings("Search mods"),
|
||||
imp._findSearchFocus == true,
|
||||
function() imp:_toggleFindSearchFocus() end)
|
||||
cy = cy + fieldH + math.floor(8 * m.s)
|
||||
|
||||
if #rows == 0 then
|
||||
Kit.emptyBox(x, cy, w, math.floor(110 * m.s),
|
||||
(total == 0) and Strings("This index lists no mods yet.")
|
||||
or Strings("No mods match that search."))
|
||||
local empty
|
||||
if carts then
|
||||
empty = (total == 0) and Strings("This index lists no carts yet.")
|
||||
or Strings("No carts match that search.")
|
||||
else
|
||||
empty = (total == 0) and Strings("This index lists no mods yet.")
|
||||
or Strings("No mods match that search.")
|
||||
end
|
||||
Kit.emptyBox(x, cy, w, math.floor(110 * m.s), empty)
|
||||
return (cy - y) + math.floor(110 * m.s)
|
||||
end
|
||||
|
||||
@@ -2995,7 +3233,7 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
if imp:_findThumbPending(entry.id) then
|
||||
Kit.spinner(px + thumb / 2, ly + thumb / 2, thumb * 0.28)
|
||||
else
|
||||
Kit.textCenter("micro", "MOD", px,
|
||||
Kit.textCenter("micro", carts and "CART" or "MOD", px,
|
||||
ly + (thumb - Kit.textHeight("micro")) / 2, thumb, PAL.faint)
|
||||
end
|
||||
end
|
||||
@@ -3018,7 +3256,12 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
or nil
|
||||
local rest = {}
|
||||
if entry.author then rest[#rest + 1] = entry.author end
|
||||
if entry.categories and entry.categories[1] then
|
||||
if carts then
|
||||
-- A cart has no categories; the game it plays as and its seal are what
|
||||
-- a reader is actually choosing between.
|
||||
rest[#rest + 1] = gameLabel(entry.base)
|
||||
if entry.seal then rest[#rest + 1] = entry.seal end
|
||||
elseif entry.categories and entry.categories[1] then
|
||||
rest[#rest + 1] = entry.categories[1]
|
||||
end
|
||||
if dates then
|
||||
@@ -3300,6 +3543,8 @@ local function buildConfirmModal(imp, m)
|
||||
imp._modConfirm = nil
|
||||
if c.indexEntry then
|
||||
imp:_findInstall(c.indexEntry)
|
||||
elseif c.kind == "cartPins" then
|
||||
imp:_installCartPins(c.version, c.id)
|
||||
elseif c.kind == "update" then
|
||||
imp:_confirmModUpdate(c.id, c.release)
|
||||
elseif c.kind == "enableAll" then
|
||||
@@ -3624,12 +3869,15 @@ local function buildModHeaderActionsModal(imp, m)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(380 * m.s)
|
||||
local gap = math.floor(8 * m.s)
|
||||
-- same gate as the header cluster: a cart's mod set is not bulk-editable
|
||||
local bulkOk = not imp.safeMode
|
||||
and not (imp.modCartPlan and imp:modCartPlan())
|
||||
local btns = {
|
||||
{ label = Strings("Mod profiles..."), action = function() imp._profilesPopup = true end },
|
||||
{ label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end },
|
||||
{ label = Strings("Enable all mods"), kind = "good", enabled = not imp.safeMode,
|
||||
{ label = Strings("Enable all mods"), kind = "good", enabled = bulkOk,
|
||||
action = function() imp:_setAllMods(true) end },
|
||||
{ label = Strings("Disable all mods"), kind = "warn", enabled = not imp.safeMode,
|
||||
{ label = Strings("Disable all mods"), kind = "warn", enabled = bulkOk,
|
||||
action = function() imp:_setAllMods(false) end },
|
||||
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end },
|
||||
}
|
||||
@@ -3760,8 +4008,10 @@ local function buildGameModal(imp, m)
|
||||
action = function() imp._gamePopup = nil end })
|
||||
end
|
||||
|
||||
local SEAL_WORD = { open = "open", ["sealed+"] = "sealed+" }
|
||||
|
||||
local function cartRowLabel(row)
|
||||
local seal = (row.seal == "open") and Strings("open") or Strings("sealed")
|
||||
local seal = Strings(SEAL_WORD[row.seal] or "sealed")
|
||||
return Strings("%s - v%s - %s", tostring(row.title or row.id),
|
||||
tostring(row.version or "?"), seal)
|
||||
end
|
||||
@@ -3828,11 +4078,12 @@ local function buildCartModal(imp, m)
|
||||
Kit.pager(px + pad, cy, pw - 2 * pad, cur, #rows, perPage, pageKey))
|
||||
cy = cy + pagerH + gap
|
||||
end
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, rowH, "cartpop-more",
|
||||
Strings("Get more carts"), { kind = "accent", font = "small",
|
||||
action = function()
|
||||
imp._cartNotice = Strings("Browsing for carts arrives in a later update.")
|
||||
end })
|
||||
FilePicker.available() and Strings("Import a cart")
|
||||
or Strings("Get more carts"),
|
||||
{ kind = "accent", font = "small",
|
||||
action = function() imp:importCartFile(version) end })
|
||||
cy = cy + rowH + gap
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, rowH, "cartpop-close",
|
||||
Strings("Close"), { font = "small",
|
||||
@@ -3949,12 +4200,17 @@ local function buildCartSaveModal(imp, m)
|
||||
PAL.muted)
|
||||
end
|
||||
|
||||
-- Category filter for FIND MODS. Two columns, because an index can list
|
||||
-- enough categories to overflow a single stacked column on a short window.
|
||||
-- Category filter for FIND MODS, base-game filter for FIND CARTS (a cart has
|
||||
-- no categories). Two columns, because an index can list enough categories
|
||||
-- to overflow a single stacked column on a short window.
|
||||
local function buildFilterModal(imp, m)
|
||||
local cats = (imp.findIndex and imp.findIndex.categories) or {}
|
||||
local carts = imp.findKind == "carts"
|
||||
local keys = (imp.findIndex
|
||||
and (carts and imp.findIndex.baseGames or imp.findIndex.categories)) or {}
|
||||
local items = { { key = nil, label = Strings("All") } }
|
||||
for _, c in ipairs(cats) do items[#items + 1] = { key = c, label = c } end
|
||||
for _, c in ipairs(keys) do
|
||||
items[#items + 1] = { key = c, label = carts and gameLabel(c) or c }
|
||||
end
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(440 * m.s)
|
||||
local gap = math.floor(8 * m.s)
|
||||
@@ -3963,18 +4219,20 @@ local function buildFilterModal(imp, m)
|
||||
+ nrows * (m.btnH + gap) + m.btnH + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local cy = py + pad
|
||||
Kit.text("button", Strings("Filter by category"), px + pad, cy, PAL.heading)
|
||||
Kit.text("button", carts and Strings("Filter by base game")
|
||||
or Strings("Filter by category"), px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
local colW = math.floor((pw - 2 * pad - gap) / 2)
|
||||
local active = carts and imp.findBase or imp.findCategory
|
||||
for i, it in ipairs(items) do
|
||||
local bx = px + pad + ((i - 1) % 2) * (colW + gap)
|
||||
local by = cy + math.floor((i - 1) / 2) * (m.btnH + gap)
|
||||
local key = it.key
|
||||
btn(imp, bx, by, colW, m.btnH, "filterpop-" .. (key or "all"), it.label, {
|
||||
kind = (imp.findCategory == key) and "primary" or "ghost",
|
||||
kind = (active == key) and "primary" or "ghost",
|
||||
font = "small",
|
||||
action = function()
|
||||
imp.findCategory = key
|
||||
if carts then imp.findBase = key else imp.findCategory = key end
|
||||
setPage(imp, "find", 1)
|
||||
imp._filterPopup = nil
|
||||
end })
|
||||
@@ -4332,8 +4590,14 @@ local function buildFindEntryModal(imp, m)
|
||||
end
|
||||
trend = (#trend > 0) and table.concat(trend, " - ") or nil
|
||||
local trendH = trend and (Kit.textHeight("small") + math.floor(2 * m.s)) or 0
|
||||
-- What a cart actually is: a pinned mod list. Its own page installs them;
|
||||
-- this popup only ever installs the cart file.
|
||||
local pins = ModIndex.isCart(entry)
|
||||
and Strings("Pins %d mod(s) - install them from the cart's own page",
|
||||
#(entry.mods or {})) or nil
|
||||
local pinsH = pins and (Kit.textHeight("small") + math.floor(2 * m.s)) or 0
|
||||
local h = pad + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
+ Kit.textHeight("small") + trendH + noteH + math.floor(12 * m.s)
|
||||
+ Kit.textHeight("small") + trendH + pinsH + noteH + math.floor(12 * m.s)
|
||||
+ nBtns * (m.btnH + gap) - gap + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local cy = py + pad
|
||||
@@ -4342,7 +4606,10 @@ local function buildFindEntryModal(imp, m)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
local lead = "v" .. tostring(ModIndex.displayVersion(entry))
|
||||
if entry.author then lead = lead .. " - " .. entry.author end
|
||||
if entry.categories and entry.categories[1] then
|
||||
if ModIndex.isCart(entry) then
|
||||
lead = lead .. " - " .. gameLabel(entry.base)
|
||||
if entry.seal then lead = lead .. " - " .. entry.seal end
|
||||
elseif entry.categories and entry.categories[1] then
|
||||
lead = lead .. " - " .. entry.categories[1]
|
||||
end
|
||||
local dl = stats and ModUpdate.downloadsShort(stats.total) or nil
|
||||
@@ -4359,6 +4626,12 @@ local function buildFindEntryModal(imp, m)
|
||||
px + pad, cy, PAL.muted)
|
||||
cy = cy + Kit.textHeight("small")
|
||||
end
|
||||
if pins then
|
||||
cy = cy + math.floor(2 * m.s)
|
||||
Kit.text("small", Kit.ellipsize("small", pins, pw - 2 * pad),
|
||||
px + pad, cy, PAL.muted)
|
||||
cy = cy + Kit.textHeight("small")
|
||||
end
|
||||
if note then
|
||||
cy = cy + math.floor(4 * m.s)
|
||||
Kit.text("small", note, px + pad, cy, PAL.green)
|
||||
|
||||
@@ -1386,11 +1386,9 @@ end
|
||||
-- tiles, no sheet of its own. So the rows here point at the icon sheets
|
||||
-- extractIcons already writes rather than at a second copy of them.
|
||||
--
|
||||
-- One frame, not two. _DoesSpriteHaveFacings sends everything from
|
||||
-- SPRITE_POKEMON up to .only_down, and a doll is a still object that never
|
||||
-- steps, so the only OAM set it ever uses is FacingStepDown0 -- tiles $00..$03,
|
||||
-- the icon's FIRST frame (data/sprites/facings.asm). The second frame is the
|
||||
-- party menu's bob and never reaches the map.
|
||||
-- Two frames: SPRITEMOVEDATA_POKEMON's OBJECT_ACTION_BOUNCE swaps
|
||||
-- FacingStepDown0's tiles $00..$03 for FacingStepUp0's $04..$07 (#1748).
|
||||
-- data/sprites/map_objects.asm:181-187, engine/overworld/map_object_action.asm:184
|
||||
--
|
||||
-- Palette 0 because _GetSpritePalette answers `xor a` for every mon sprite,
|
||||
-- which is PAL_OW_RED in the MapObjectPals set.
|
||||
@@ -1418,7 +1416,7 @@ function RomExtractorGen2:extractMonSprites(out)
|
||||
id = constName,
|
||||
source = ("ROM:SpriteMons[%d]"):format(row),
|
||||
image = "assets/generated/icons/gen2/" .. base .. ".png",
|
||||
frames = 1,
|
||||
frames = 2,
|
||||
walker = false,
|
||||
spriteType = "POKEMON_SPRITE",
|
||||
palette = SPRITE_PALETTE_NAME[0],
|
||||
@@ -4039,6 +4037,12 @@ function RomExtractorGen2:splashGfx()
|
||||
local fade = self:symbol("GameFreakDittoPaletteFade")
|
||||
out.dittoFade = self:colors(fade.bank, fade.address, 16)
|
||||
end
|
||||
-- gfx/splash/ditto.pal, the OBJ palette _CGB_GamefreakLogo loads
|
||||
-- (../pokecrystal/engine/gfx/cgb_layouts.asm:876-893).
|
||||
if self.symbols["_CGB_GamefreakLogo.GamefreakDittoPalette"] then
|
||||
local pal = self:symbol("_CGB_GamefreakLogo.GamefreakDittoPalette")
|
||||
out.dittoPalette = self:colors(pal.bank, pal.address, 4)
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
@@ -5844,6 +5848,21 @@ function RomExtractorGen2:extractMenuGfx()
|
||||
8, 8, "emotes/grass_rustle.png", true)
|
||||
emotes.grassRustle = "assets/generated/emotes/grass_rustle.png"
|
||||
end
|
||||
-- LoadFishingGFX (engine/events/fishing_gfx.asm:1-21): three 16x8 pose rows
|
||||
-- over the standing frames' bottom tiles, then the rod tiles $fc/$fd (#1708).
|
||||
local fishing = self.symbols["FishingGFX"]
|
||||
if fishing then
|
||||
self:write2bpp(self.rom:bytes(fishing[1], fishing[2], 8 * 16),
|
||||
16, 32, "emotes/fishing.png", true)
|
||||
emotes.fishing = "assets/generated/emotes/fishing.png"
|
||||
end
|
||||
-- ../pokecrystal/engine/events/fishing_gfx.asm:41, Kris' half of that art.
|
||||
local krisFishing = self.symbols["KrisFishingGFX"]
|
||||
if krisFishing then
|
||||
self:write2bpp(self.rom:bytes(krisFishing[1], krisFishing[2], 8 * 16),
|
||||
16, 32, "emotes/fishing_female.png", true)
|
||||
emotes.fishingFemale = "assets/generated/emotes/fishing_female.png"
|
||||
end
|
||||
out.emotes = emotes
|
||||
|
||||
-- The Pokecenter heal machine's OBJ art (engine/events/heal_machine_anim
|
||||
|
||||
+719
-54
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user