mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c11c762f15 | |||
| 780246c4f6 | |||
| 5714555847 | |||
| f5b8b6c85f | |||
| db25c14dfb | |||
| 90163a3ff2 | |||
| 4bdb9435a4 |
@@ -14,6 +14,7 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
||||
* **Screen position setting** (center, upper, top) shared across all games, for clamp-on controllers that cover the lower screen
|
||||
* **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders
|
||||
* **Pokédex diploma and printer image exports**
|
||||
* **Shareable mod lists** over save sync, optionally carrying the options set for those mods, which the receiving device is asked about before anything is changed
|
||||
|
||||
## Gen 2 Specifics
|
||||
|
||||
|
||||
@@ -63,8 +63,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
ndk {
|
||||
|
||||
@@ -12,13 +12,6 @@
|
||||
"tintColor": "3b5ca8",
|
||||
"category": "games",
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.2.12",
|
||||
"date": "2026-08-20",
|
||||
"size": 13737259,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.12/gen1recomp++-0.2.12-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1582 Sync not working between steamdeck and windows\n- #1583 Can’t sync between iOS and windows\n\n## Contributors\n\n- @AverageConsumer\n- @bryanthaboi\n- @thibautbus"
|
||||
},
|
||||
{
|
||||
"version": "0.2.11",
|
||||
"date": "2026-08-20",
|
||||
|
||||
@@ -125,6 +125,26 @@ function ImageWriter.columnsToRows(raw, tilesWide, tilesHigh, bytesPerTile)
|
||||
return out
|
||||
end
|
||||
|
||||
-- Inverse of pret tools/gfx --interleave (pokecrystal tools/gfx.c).
|
||||
-- Build-time interleave stores each vertical 8x16 pair as consecutive 8x8
|
||||
-- tiles for OBJ mode; this restores row-major sheet order for PNGs.
|
||||
function ImageWriter.deinterleave(raw, width, bytesPerTile)
|
||||
bytesPerTile = bytesPerTile or 16
|
||||
local widthTiles = width / 8
|
||||
local numTiles = #raw / bytesPerTile
|
||||
local out = {}
|
||||
for i = 0, numTiles - 1 do
|
||||
local row = math.floor(i / widthTiles)
|
||||
local src = i * 2 - (row % 2 == 1
|
||||
and widthTiles * (row + 1) - 1
|
||||
or widthTiles * row)
|
||||
for offset = 1, bytesPerTile do
|
||||
out[i * bytesPerTile + offset] = raw[src * bytesPerTile + offset]
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function ImageWriter.save(image, path)
|
||||
local ok, fileData = pcall(image.encode, image, "png")
|
||||
if not ok then error("could not encode " .. path .. ": " .. tostring(fileData)) end
|
||||
|
||||
+204
-92
@@ -4362,21 +4362,70 @@ local function syncTitle(imp, m, px, py, pw, pad)
|
||||
return py + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
end
|
||||
|
||||
local function syncStatus(imp, m, x, y, w, eng)
|
||||
local function syncWidth(m, want)
|
||||
return math.floor(math.min(want, m.W - 2 * m.pad))
|
||||
end
|
||||
|
||||
local function syncFit(m, fixed, rows, gaps, texts)
|
||||
local fit = { btnH = m.btnH, gap = math.floor(8 * m.s), lines = {} }
|
||||
local avail = m.H - 2 * m.pad
|
||||
texts = texts or {}
|
||||
for i, blk in ipairs(texts) do fit.lines[i] = blk.max end
|
||||
local function total()
|
||||
local t = fixed + rows * fit.btnH + gaps * fit.gap
|
||||
for i, blk in ipairs(texts) do
|
||||
t = t + Kit.wrapHeight(blk.font, blk.str, blk.w, fit.lines[i])
|
||||
end
|
||||
return t
|
||||
end
|
||||
while total() > avail do
|
||||
local worst, worstH = nil, 0
|
||||
for i, blk in ipairs(texts) do
|
||||
if fit.lines[i] > 1 then
|
||||
local hgt = Kit.wrapHeight(blk.font, blk.str, blk.w, fit.lines[i])
|
||||
if hgt > worstH then worst, worstH = i, hgt end
|
||||
end
|
||||
end
|
||||
if not worst then break end
|
||||
fit.lines[worst] = fit.lines[worst] - 1
|
||||
end
|
||||
if total() > avail and gaps > 0 then
|
||||
fit.gap = math.max(math.max(2, math.floor(3 * m.s)),
|
||||
fit.gap - math.ceil((total() - avail) / gaps))
|
||||
end
|
||||
if total() > avail and rows > 0 then
|
||||
fit.btnH = math.max(Kit.tapMin(),
|
||||
fit.btnH - math.ceil((total() - avail) / rows))
|
||||
end
|
||||
fit.over = total() - avail
|
||||
fit.h = math.min(total(), avail)
|
||||
return fit
|
||||
end
|
||||
|
||||
local function syncStatus(imp, m, x, y, w, eng, fit)
|
||||
local bh = (fit and fit.btnH) or m.btnH
|
||||
local gap = (fit and fit.gap) or math.floor(8 * m.s)
|
||||
if eng:busy() then
|
||||
Loader.inline(x, y, w, m.btnH, eng.status)
|
||||
return m.btnH + math.floor(8 * m.s)
|
||||
Loader.inline(x, y, w, bh, eng.status)
|
||||
return bh + gap
|
||||
end
|
||||
Kit.text("small", Kit.ellipsize("small", eng.status or "", w), x, y,
|
||||
eng.phase == "error" and PAL.red or PAL.muted)
|
||||
return Kit.textHeight("small") + gap + math.floor(2 * m.s)
|
||||
end
|
||||
|
||||
local function syncReserve(m, eng)
|
||||
if eng:busy() then return m.btnH + math.floor(8 * m.s) end
|
||||
return Kit.textHeight("small") + math.floor(10 * m.s)
|
||||
end
|
||||
|
||||
local function syncRow(imp, m, x, y, w, key, label, opts)
|
||||
local function syncRow(imp, m, x, y, w, key, label, opts, fit)
|
||||
opts = opts or {}
|
||||
opts.font = "small"
|
||||
btn(imp, x, y, w, m.btnH, key, label, opts)
|
||||
return y + m.btnH + math.floor(8 * m.s)
|
||||
local bh = (fit and fit.btnH) or m.btnH
|
||||
local gap = (fit and fit.gap) or math.floor(8 * m.s)
|
||||
btn(imp, x, y, w, bh, key, label, opts)
|
||||
return y + bh + gap
|
||||
end
|
||||
|
||||
function LauncherView.syncSideText(meta)
|
||||
@@ -4408,91 +4457,99 @@ end
|
||||
local function buildSyncConflict(imp, m, eng)
|
||||
local row = eng.conflicts[1]
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(520 * m.s)
|
||||
local w = syncWidth(m, math.floor(520 * m.s))
|
||||
local innerW = w - 2 * pad
|
||||
local lead = row.overlap
|
||||
and Strings("These saves were played at the same time.")
|
||||
or Strings("This save also changed on another device.")
|
||||
local leadH = Kit.wrapHeight("small", lead, innerW, 2)
|
||||
local sideH = Kit.textHeight("small") + math.floor(2 * m.s)
|
||||
+ Kit.wrapHeight("micro", "x", innerW, 2)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s) + leadH
|
||||
+ math.floor(10 * m.s) + 2 * (sideH + math.floor(10 * m.s))
|
||||
+ 4 * (m.btnH + math.floor(8 * m.s)) + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local mine = LauncherView.syncSideText(row.localMeta)
|
||||
local theirs = LauncherView.syncSideText(row.remoteMeta)
|
||||
local fit = syncFit(m,
|
||||
2 * pad + Kit.textHeight("button") + math.floor(22 * m.s)
|
||||
+ 2 * (Kit.textHeight("small") + math.floor(12 * m.s)),
|
||||
4, 4, {
|
||||
{ font = "small", str = lead, w = innerW, max = 2 },
|
||||
{ font = "micro", str = mine, w = innerW, max = 2 },
|
||||
{ font = "micro", str = theirs, w = innerW, max = 2 },
|
||||
})
|
||||
local px, py, pw = modalPanel(m, w, fit.h)
|
||||
local cy = syncTitle(imp, m, px, py + pad, pw, pad)
|
||||
cy = cy + Kit.textWrapped("small", lead, px + pad, cy, pw - 2 * pad,
|
||||
PAL.detail, 2) + math.floor(10 * m.s)
|
||||
cy = cy + Kit.textWrapped("small", lead, px + pad, cy, innerW,
|
||||
PAL.detail, fit.lines[1]) + math.floor(10 * m.s)
|
||||
|
||||
local function side(title, meta)
|
||||
local function side(title, text, lines)
|
||||
Kit.text("small", title, px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("small") + math.floor(2 * m.s)
|
||||
cy = cy + Kit.textWrapped("micro", LauncherView.syncSideText(meta),
|
||||
px + pad, cy, pw - 2 * pad, PAL.muted, 2) + math.floor(10 * m.s)
|
||||
cy = cy + Kit.textWrapped("micro", text, px + pad, cy, innerW,
|
||||
PAL.muted, lines) + math.floor(10 * m.s)
|
||||
end
|
||||
side(Strings("This device") .. " \194\183 " .. tostring(row.version or "?"),
|
||||
row.localMeta)
|
||||
side(Strings("Other device"), row.remoteMeta)
|
||||
mine, fit.lines[2])
|
||||
side(Strings("Other device"), theirs, fit.lines[3])
|
||||
|
||||
local key = row.key
|
||||
cy = syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-keep-this",
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-keep-this",
|
||||
Strings("Keep this device"), { kind = "primary",
|
||||
action = function() imp:_syncResolve(key, "local") end })
|
||||
cy = syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-keep-other",
|
||||
action = function() imp:_syncResolve(key, "local") end }, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-keep-other",
|
||||
Strings("Keep the other device"), { kind = "accent",
|
||||
action = function() imp:_syncResolve(key, "remote") end })
|
||||
cy = syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-keep-both",
|
||||
action = function() imp:_syncResolve(key, "remote") end }, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-keep-both",
|
||||
Strings("Keep both"), {
|
||||
action = function() imp:_syncResolve(key, "both") end })
|
||||
syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-conflict-close",
|
||||
Strings("Close"), { action = function() imp:_closeSync() end })
|
||||
action = function() imp:_syncResolve(key, "both") end }, fit)
|
||||
syncRow(imp, m, px + pad, cy, innerW, "sync-conflict-close",
|
||||
Strings("Close"), { action = function() imp:_closeSync() end }, fit)
|
||||
end
|
||||
|
||||
local function buildSyncLink(imp, m, eng)
|
||||
local mo = imp._syncModal
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(460 * m.s)
|
||||
local fieldH = math.max(Kit.tapMin(), math.floor(36 * m.s))
|
||||
local w = syncWidth(m, math.floor(460 * m.s))
|
||||
local innerW = w - 2 * pad
|
||||
local hint = Strings("Enter the two codes the other device is showing.")
|
||||
local hintH = Kit.wrapHeight("small", hint, w - 2 * pad, 2)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s) + hintH
|
||||
+ math.floor(10 * m.s) + 2 * (fieldH + math.floor(8 * m.s))
|
||||
+ Kit.textHeight("small") + math.floor(10 * m.s)
|
||||
+ 2 * (m.btnH + math.floor(8 * m.s)) + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local fieldH = math.max(Kit.tapMin(), math.floor(36 * m.s))
|
||||
local fit = syncFit(m,
|
||||
2 * pad + Kit.textHeight("button") + math.floor(22 * m.s)
|
||||
+ 2 * (fieldH + math.floor(8 * m.s)) + syncReserve(m, eng),
|
||||
2, 2, { { font = "small", str = hint, w = innerW, max = 2 } })
|
||||
local px, py, pw = modalPanel(m, w, fit.h)
|
||||
local cy = syncTitle(imp, m, px, py + pad, pw, pad)
|
||||
cy = cy + Kit.textWrapped("small", hint, px + pad, cy, pw - 2 * pad,
|
||||
PAL.detail, 2) + math.floor(10 * m.s)
|
||||
textField(imp, px + pad, cy, pw - 2 * pad, fieldH, "sync-code1",
|
||||
cy = cy + Kit.textWrapped("small", hint, px + pad, cy, innerW,
|
||||
PAL.detail, fit.lines[1]) + math.floor(10 * m.s)
|
||||
textField(imp, px + pad, cy, innerW, fieldH, "sync-code1",
|
||||
mo.code1 or "", Strings("First code"), imp._syncFocus == "code1",
|
||||
function() imp:_syncFocusField("code1") end)
|
||||
cy = cy + fieldH + math.floor(8 * m.s)
|
||||
textField(imp, px + pad, cy, pw - 2 * pad, fieldH, "sync-code2",
|
||||
cy = cy + fieldH + fit.gap
|
||||
textField(imp, px + pad, cy, innerW, fieldH, "sync-code2",
|
||||
mo.code2 or "", Strings("Second code"), imp._syncFocus == "code2",
|
||||
function() imp:_syncFocusField("code2") end)
|
||||
cy = cy + fieldH + math.floor(8 * m.s)
|
||||
cy = cy + syncStatus(imp, m, px + pad, cy, pw - 2 * pad, eng)
|
||||
cy = syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-link-go",
|
||||
cy = cy + fieldH + fit.gap
|
||||
cy = cy + syncStatus(imp, m, px + pad, cy, innerW, eng, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-link-go",
|
||||
Strings("Link this device"), { kind = "primary", enabled = not eng:busy(),
|
||||
action = function() imp:_syncLink() end })
|
||||
syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-link-back",
|
||||
Strings("Back"), { action = function() imp:_syncView("home") end })
|
||||
action = function() imp:_syncLink() end }, fit)
|
||||
syncRow(imp, m, px + pad, cy, innerW, "sync-link-back",
|
||||
Strings("Back"), { action = function() imp:_syncView("home") end }, fit)
|
||||
end
|
||||
|
||||
local function buildSyncMods(imp, m, eng)
|
||||
local mo = imp._syncModal
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(500 * m.s)
|
||||
local w = syncWidth(m, math.floor(500 * m.s))
|
||||
local innerW = w - 2 * pad
|
||||
local fieldH = math.max(Kit.tapMin(), math.floor(36 * m.s))
|
||||
local plan = eng.modPlan
|
||||
local rows = 4 + (plan and 1 or 0)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
+ 3 * (Kit.textHeight("small") + math.floor(8 * m.s))
|
||||
+ fieldH + math.floor(8 * m.s)
|
||||
+ rows * (m.btnH + math.floor(8 * m.s)) + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local codeH = eng.shareCode and (Kit.textHeight("small")
|
||||
+ Kit.textHeight("stat") + Kit.textHeight("micro")
|
||||
+ math.floor(18 * m.s)) or 0
|
||||
local planH = plan and (Kit.textHeight("small") + math.floor(8 * m.s)) or 0
|
||||
local fit = syncFit(m,
|
||||
2 * pad + Kit.textHeight("button") + math.floor(12 * m.s) + codeH + planH
|
||||
+ fieldH + math.floor(8 * m.s) + syncReserve(m, eng),
|
||||
rows, rows, {})
|
||||
local px, py, pw = modalPanel(m, w, fit.h)
|
||||
local cy = syncTitle(imp, m, px, py + pad, pw, pad)
|
||||
local innerW = pw - 2 * pad
|
||||
|
||||
if eng.shareCode then
|
||||
Kit.text("small", Strings("Share this code:"), px + pad, cy, PAL.muted)
|
||||
@@ -4504,17 +4561,23 @@ local function buildSyncMods(imp, m, eng)
|
||||
px + pad, cy, PAL.muted)
|
||||
cy = cy + Kit.textHeight("micro") + math.floor(10 * m.s)
|
||||
end
|
||||
local withOptions = mo.withOptions ~= false
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-share-options",
|
||||
Strings("Include my mod options") .. " \194\183 "
|
||||
.. (withOptions and Strings("ON") or Strings("OFF")),
|
||||
{ kind = withOptions and "accent" or nil, enabled = not eng:busy(),
|
||||
action = function() imp:_syncToggleShareOptions() end }, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-share-mods",
|
||||
Strings("Share mod list"), { kind = "accent", enabled = not eng:busy(),
|
||||
action = function() imp:_syncShareMods() end })
|
||||
action = function() imp:_syncShareMods() end }, fit)
|
||||
|
||||
textField(imp, px + pad, cy, innerW, fieldH, "sync-share-code",
|
||||
mo.share or "", Strings("Paste a 6-character mod code"),
|
||||
imp._syncFocus == "share", function() imp:_syncFocusField("share") end)
|
||||
cy = cy + fieldH + math.floor(8 * m.s)
|
||||
cy = cy + fieldH + fit.gap
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-get-mods",
|
||||
Strings("Get mod list"), { kind = "accent", enabled = not eng:busy(),
|
||||
action = function() imp:_syncGetShare() end })
|
||||
action = function() imp:_syncGetShare() end }, fit)
|
||||
|
||||
if plan then
|
||||
local line = Strings("%d mods, %d indexes to add",
|
||||
@@ -4523,24 +4586,29 @@ local function buildSyncMods(imp, m, eng)
|
||||
line = line .. " \194\183 " .. Strings("%d not in your indexes",
|
||||
#plan.missing)
|
||||
end
|
||||
if #(plan.options or {}) > 0 then
|
||||
line = line .. " \194\183 " .. (plan.applyOptions
|
||||
and Strings("options for %d mods", #plan.options)
|
||||
or Strings("their options skipped"))
|
||||
end
|
||||
Kit.text("small", Kit.ellipsize("small", line, innerW), px + pad, cy,
|
||||
PAL.detail)
|
||||
cy = cy + Kit.textHeight("small") + math.floor(8 * m.s)
|
||||
local prog = mo.progress
|
||||
if prog then
|
||||
Loader.inline(px + pad, cy, innerW, m.btnH,
|
||||
Loader.inline(px + pad, cy, innerW, fit.btnH,
|
||||
Strings("%d of %d", prog.done or 0, prog.total or 0))
|
||||
cy = cy + m.btnH + math.floor(8 * m.s)
|
||||
cy = cy + fit.btnH + fit.gap
|
||||
else
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-apply-mods",
|
||||
Strings("Apply these mods"), { kind = "primary",
|
||||
enabled = not eng:busy(),
|
||||
action = function() imp:_syncApplyMods() end })
|
||||
action = function() imp:_syncApplyMods() end }, fit)
|
||||
end
|
||||
end
|
||||
cy = cy + syncStatus(imp, m, px + pad, cy, innerW, eng)
|
||||
cy = cy + syncStatus(imp, m, px + pad, cy, innerW, eng, fit)
|
||||
syncRow(imp, m, px + pad, cy, innerW, "sync-mods-back", Strings("Back"),
|
||||
{ action = function() imp:_syncView("home") end })
|
||||
{ action = function() imp:_syncView("home") end }, fit)
|
||||
end
|
||||
|
||||
function LauncherView.syncDeviceRows(eng, limit)
|
||||
@@ -4562,29 +4630,35 @@ end
|
||||
|
||||
local function buildSyncHome(imp, m, eng)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(460 * m.s)
|
||||
local w = syncWidth(m, math.floor(460 * m.s))
|
||||
local linked = eng:linked()
|
||||
local codes = eng.codes
|
||||
local body = linked
|
||||
and Strings("This device is linked. Saves sync when the launcher opens, a few seconds after each save, and every few minutes while the app is running.")
|
||||
or Strings(SYNC_HINT)
|
||||
local innerW = w - 2 * pad
|
||||
local hintH = Kit.wrapHeight("small", body, innerW, 5)
|
||||
local codesH = codes
|
||||
and (Kit.textHeight("small") + math.floor(6 * m.s)
|
||||
+ 2 * (Kit.textHeight("title") + math.floor(4 * m.s))
|
||||
+ math.floor(8 * m.s)) or 0
|
||||
local devices = linked and LauncherView.syncDeviceRows(eng) or {}
|
||||
local hidden, fit = 0, nil
|
||||
repeat
|
||||
local devicesH = #devices > 0
|
||||
and (Kit.textHeight("small") + math.floor(6 * m.s)) or 0
|
||||
local rows = (linked and 5 or 3) + #devices
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s) + hintH
|
||||
+ math.floor(10 * m.s) + codesH + devicesH + m.btnH + math.floor(10 * m.s)
|
||||
+ rows * (m.btnH + math.floor(8 * m.s)) + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
fit = syncFit(m,
|
||||
2 * pad + Kit.textHeight("button") + math.floor(22 * m.s) + codesH
|
||||
+ devicesH + syncReserve(m, eng),
|
||||
(linked and 4 or 3) + #devices, (linked and 4 or 3) + #devices,
|
||||
{ { font = "small", str = body, w = innerW, max = 5 } })
|
||||
if fit.over <= 0 or #devices == 0 then break end
|
||||
table.remove(devices)
|
||||
hidden = hidden + 1
|
||||
until false
|
||||
local px, py, pw = modalPanel(m, w, fit.h)
|
||||
local cy = syncTitle(imp, m, px, py + pad, pw, pad)
|
||||
cy = cy + Kit.textWrapped("small", body, px + pad, cy, innerW, PAL.detail, 5)
|
||||
+ math.floor(10 * m.s)
|
||||
cy = cy + Kit.textWrapped("small", body, px + pad, cy, innerW, PAL.detail,
|
||||
fit.lines[1]) + math.floor(10 * m.s)
|
||||
|
||||
if codes then
|
||||
Kit.text("small", Strings("Enter these on your other device:"), px + pad,
|
||||
@@ -4595,23 +4669,24 @@ local function buildSyncHome(imp, m, eng)
|
||||
Kit.text("title", codes.code2, px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("title") + math.floor(8 * m.s)
|
||||
end
|
||||
cy = cy + syncStatus(imp, m, px + pad, cy, innerW, eng)
|
||||
cy = cy + syncStatus(imp, m, px + pad, cy, innerW, eng, fit)
|
||||
|
||||
if #devices > 0 then
|
||||
Kit.text("small", Strings("Devices on this account:"), px + pad, cy,
|
||||
PAL.muted)
|
||||
Kit.text("small", hidden > 0
|
||||
and Strings("Devices on this account (%d more)", hidden)
|
||||
or Strings("Devices on this account:"), px + pad, cy, PAL.muted)
|
||||
cy = cy + Kit.textHeight("small") + math.floor(6 * m.s)
|
||||
for i, device in ipairs(devices) do
|
||||
local id = device.id
|
||||
if device.current then
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-device-" .. i,
|
||||
device.label .. " \194\183 " .. Strings("this device"),
|
||||
{ enabled = false })
|
||||
{ enabled = false }, fit)
|
||||
else
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-device-" .. i,
|
||||
Strings("Unlink %s", device.label), { kind = "danger",
|
||||
enabled = not eng:busy(),
|
||||
action = function() imp:_syncUnlinkDevice(id) end })
|
||||
action = function() imp:_syncUnlinkDevice(id) end }, fit)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4619,38 +4694,69 @@ local function buildSyncHome(imp, m, eng)
|
||||
if linked then
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-now", Strings("Sync now"),
|
||||
{ kind = "primary", enabled = not eng:busy(),
|
||||
action = function() imp:_syncNow() end })
|
||||
action = function() imp:_syncNow() end }, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-mods",
|
||||
Strings("Share or get a mod list"), { kind = "accent",
|
||||
action = function() imp:_syncView("mods") end })
|
||||
action = function() imp:_syncView("mods") end }, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-unlink",
|
||||
Strings("Unlink this device"), { kind = "danger",
|
||||
action = function() imp:_syncUnlink() end })
|
||||
action = function() imp:_syncUnlink() end }, fit)
|
||||
else
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-create",
|
||||
Strings("Create sync account"), { kind = "primary",
|
||||
enabled = not eng:busy(),
|
||||
action = function() imp:_syncCreate() end })
|
||||
action = function() imp:_syncCreate() end }, fit)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-link",
|
||||
Strings("Link this device"), { kind = "accent",
|
||||
action = function() imp:_syncView("link") end })
|
||||
action = function() imp:_syncView("link") end }, fit)
|
||||
end
|
||||
syncRow(imp, m, px + pad, cy, innerW, "sync-close", Strings("Close"),
|
||||
{ action = function() imp:_closeSync() end })
|
||||
{ action = function() imp:_closeSync() end }, fit)
|
||||
end
|
||||
|
||||
local function buildSyncModOptions(imp, m, eng)
|
||||
local plan = eng.modPlan
|
||||
local ids = {}
|
||||
for _, row in ipairs(plan.options or {}) do ids[#ids + 1] = row.id end
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = syncWidth(m, math.floor(480 * m.s))
|
||||
local innerW = w - 2 * pad
|
||||
local lead = Strings(
|
||||
"This mod list also carries the options its owner set for %d mods. Import their options, or keep the ones you have?",
|
||||
#ids)
|
||||
local names = table.concat(ids, ", ")
|
||||
local fit = syncFit(m,
|
||||
2 * pad + Kit.textHeight("button") + math.floor(32 * m.s), 2, 2, {
|
||||
{ font = "small", str = lead, w = innerW, max = 4 },
|
||||
{ font = "micro", str = names, w = innerW, max = 3 },
|
||||
})
|
||||
local px, py, pw = modalPanel(m, w, fit.h)
|
||||
local cy = syncTitle(imp, m, px, py + pad, pw, pad)
|
||||
cy = cy + Kit.textWrapped("small", lead, px + pad, cy, innerW, PAL.detail,
|
||||
fit.lines[1]) + math.floor(8 * m.s)
|
||||
cy = cy + Kit.textWrapped("micro", names, px + pad, cy, innerW, PAL.muted,
|
||||
fit.lines[2]) + math.floor(12 * m.s)
|
||||
cy = syncRow(imp, m, px + pad, cy, innerW, "sync-options-import",
|
||||
Strings("Import their options"), { kind = "primary",
|
||||
action = function() imp:_syncAnswerModOptions(true) end }, fit)
|
||||
syncRow(imp, m, px + pad, cy, innerW, "sync-options-skip",
|
||||
Strings("Keep my options"), {
|
||||
action = function() imp:_syncAnswerModOptions(false) end }, fit)
|
||||
end
|
||||
|
||||
local function buildSyncUnavailable(imp, m, msg)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(420 * m.s)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
+ Kit.wrapHeight("small", msg, w - 2 * pad, 4) + math.floor(10 * m.s)
|
||||
+ m.btnH + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local w = syncWidth(m, math.floor(420 * m.s))
|
||||
local innerW = w - 2 * pad
|
||||
local fit = syncFit(m,
|
||||
2 * pad + Kit.textHeight("button") + math.floor(22 * m.s), 1, 0,
|
||||
{ { font = "small", str = msg, w = innerW, max = 4 } })
|
||||
local px, py, pw = modalPanel(m, w, fit.h)
|
||||
local cy = syncTitle(imp, m, px, py + pad, pw, pad)
|
||||
cy = cy + Kit.textWrapped("small", msg, px + pad, cy, pw - 2 * pad,
|
||||
PAL.detail, 4) + math.floor(10 * m.s)
|
||||
syncRow(imp, m, px + pad, cy, pw - 2 * pad, "sync-close",
|
||||
Strings("Close"), { action = function() imp:_closeSync() end })
|
||||
cy = cy + Kit.textWrapped("small", msg, px + pad, cy, innerW,
|
||||
PAL.detail, fit.lines[1]) + math.floor(10 * m.s)
|
||||
syncRow(imp, m, px + pad, cy, innerW, "sync-close",
|
||||
Strings("Close"), { action = function() imp:_closeSync() end }, fit)
|
||||
end
|
||||
|
||||
local function buildSyncModal(imp, m)
|
||||
@@ -4669,6 +4775,12 @@ local function buildSyncModal(imp, m)
|
||||
buildSyncConflict(imp, m, eng)
|
||||
return
|
||||
end
|
||||
local plan = eng.modPlan
|
||||
if type(plan) == "table" and #(plan.options or {}) > 0
|
||||
and plan.applyOptions == nil then
|
||||
buildSyncModOptions(imp, m, eng)
|
||||
return
|
||||
end
|
||||
local view = imp._syncModal and imp._syncModal.view or "home"
|
||||
if view == "link" then
|
||||
buildSyncLink(imp, m, eng)
|
||||
|
||||
+170
-37
@@ -5258,80 +5258,213 @@ function RomExtractorGen2:extractMenuGfx()
|
||||
end
|
||||
|
||||
-- Goldenrod Game Corner: Slot Machine graphics assets
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local function packBytes(bytes)
|
||||
local chars = {}
|
||||
for i = 1, #bytes do chars[i] = string.char(bytes[i]) end
|
||||
return table.concat(chars)
|
||||
end
|
||||
local function writeRaw(relative, bytes)
|
||||
local ok, writeError = CacheFs.write(
|
||||
"assets/generated/" .. relative, packBytes(bytes))
|
||||
if not ok then
|
||||
error("could not write " .. relative .. ": " .. tostring(writeError))
|
||||
end
|
||||
end
|
||||
|
||||
-- Canonical sheet sizes match the cart art the UI indexes (and pret's
|
||||
-- gfx/slots + gfx/card_flip PNGs). ROM LZ streams are those sheets after
|
||||
-- Makefile gfx transforms; reverse what the decompressed bytes still carry.
|
||||
local SLOTS1_W, SLOTS1_H = 16, 152
|
||||
local SLOTS2_W, SLOTS2_H = 16, 256
|
||||
local SLOTS3_W, SLOTS3_H = 24, 240
|
||||
local CARD1_W, CARD1_H = 128, 32
|
||||
local CARD2_W, CARD2_H = 24, 160
|
||||
local CARD3_W, CARD3_H = 8, 56
|
||||
|
||||
local function pad2bpp(raw, width, height)
|
||||
local need = width * height / 4
|
||||
while #raw < need do raw[#raw + 1] = 0 end
|
||||
while #raw > need do table.remove(raw) end
|
||||
return raw
|
||||
end
|
||||
|
||||
local function writeSheet(raw, width, height, relative, transparent)
|
||||
self:write2bpp(pad2bpp(raw, width, height), width, height, relative,
|
||||
transparent)
|
||||
end
|
||||
|
||||
-- Slots3LZ is unique 8x16 OBJ columns (interleave + remove-duplicates +
|
||||
-- remove-xflip). Rebuild the 24x240 actor sheet the UI quads expect from
|
||||
-- OAMData_SlotsGolem / Chansey* / Egg (data/sprite_anims/oam.asm), same
|
||||
-- pattern as title-screen Ho-Oh frame composition above.
|
||||
local function composeSlotsActors(raw)
|
||||
local tileCount = math.floor(#raw / 16)
|
||||
local tiles = {}
|
||||
for index = 0, tileCount - 1 do
|
||||
local one = {}
|
||||
for b = 1, 16 do one[b] = raw[index * 16 + b] or 0 end
|
||||
tiles[index] = ImageWriter.decode2bpp(one, 8, 8, true)
|
||||
end
|
||||
local sheet = ImageWriter.blank(SLOTS3_W, SLOTS3_H, 1, 1, 1, 0)
|
||||
local function blit8x16(tileId, dx, dy, flipX)
|
||||
local top, bot = tiles[tileId], tiles[tileId + 1]
|
||||
if not (top and bot) then return end
|
||||
ImageWriter.blit(sheet, top, dx, dy, 0, 0, 8, 8, flipX)
|
||||
ImageWriter.blit(sheet, bot, dx, dy + 8, 0, 0, 8, 8, flipX)
|
||||
end
|
||||
local function blitPose(poseY, base, entries)
|
||||
for _, e in ipairs(entries) do
|
||||
blit8x16(base + e.t, (e.x + 2) * 8, poseY + (e.y + 2) * 8, e.xf)
|
||||
end
|
||||
end
|
||||
local golem = {
|
||||
{ x = -2, y = -2, t = 0x00 }, { x = -1, y = -2, t = 0x02 },
|
||||
{ x = 0, y = -2, t = 0x00, xf = true },
|
||||
{ x = -2, y = 0, t = 0x04 }, { x = -1, y = 0, t = 0x06 },
|
||||
{ x = 0, y = 0, t = 0x04, xf = true },
|
||||
}
|
||||
local chansey = {
|
||||
{
|
||||
{ x = -2, y = -2, t = 0x00 }, { x = -1, y = -2, t = 0x02 },
|
||||
{ x = 0, y = -2, t = 0x04 },
|
||||
{ x = -2, y = 0, t = 0x06 }, { x = -1, y = 0, t = 0x08 },
|
||||
{ x = 0, y = 0, t = 0x0a },
|
||||
},
|
||||
{
|
||||
{ x = -2, y = -2, t = 0x00 }, { x = -1, y = -2, t = 0x02 },
|
||||
{ x = 0, y = -2, t = 0x04 },
|
||||
{ x = -2, y = 0, t = 0x0c }, { x = -1, y = 0, t = 0x0e },
|
||||
{ x = 0, y = 0, t = 0x10 },
|
||||
},
|
||||
{
|
||||
{ x = -2, y = -2, t = 0x00 }, { x = -1, y = -2, t = 0x02 },
|
||||
{ x = 0, y = -2, t = 0x04 },
|
||||
{ x = -2, y = 0, t = 0x12 }, { x = -1, y = 0, t = 0x14 },
|
||||
{ x = 0, y = 0, t = 0x16 },
|
||||
},
|
||||
{
|
||||
{ x = -2, y = -2, t = 0x00 }, { x = -1, y = -2, t = 0x02 },
|
||||
{ x = 0, y = -2, t = 0x04 },
|
||||
{ x = -2, y = 0, t = 0x18 }, { x = -1, y = 0, t = 0x1a },
|
||||
{ x = 0, y = 0, t = 0x1c },
|
||||
},
|
||||
{
|
||||
{ x = -2, y = -2, t = 0x1e }, { x = -1, y = -2, t = 0x20 },
|
||||
{ x = 0, y = -2, t = 0x22 },
|
||||
{ x = -2, y = 0, t = 0x24 }, { x = -1, y = 0, t = 0x26 },
|
||||
{ x = 0, y = 0, t = 0x28 },
|
||||
},
|
||||
}
|
||||
blitPose(0, 0x00, golem)
|
||||
blitPose(32, 0x08, golem)
|
||||
for index, frame in ipairs(chansey) do
|
||||
blitPose(32 + index * 32, 0x10, frame)
|
||||
end
|
||||
blit8x16(0x3a, 0, 224, false)
|
||||
return sheet
|
||||
end
|
||||
|
||||
-- card_flip_2.2bpp uses --remove-whitespace: blank tiles in column 2 of the
|
||||
-- 3-wide header strip (indices 2,5,...,23) are dropped from the ROM stream.
|
||||
-- Re-insert them so HEADER_TILE_MAP / MON_ANCHORS (pret sheet indices) work.
|
||||
local function expandCardFlip2(compact)
|
||||
local need = CARD2_W * CARD2_H / 4
|
||||
local out = {}
|
||||
for i = 1, need do out[i] = 0 end
|
||||
local whitespace = {
|
||||
[2] = true, [5] = true, [8] = true, [11] = true,
|
||||
[14] = true, [17] = true, [20] = true, [23] = true,
|
||||
}
|
||||
local src = 0
|
||||
for tile = 0, 59 do
|
||||
if not whitespace[tile] then
|
||||
for b = 1, 16 do
|
||||
out[tile * 16 + b] = compact[src * 16 + b] or 0
|
||||
end
|
||||
src = src + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local slots = nil
|
||||
if self.symbols["Slots1LZ"] then
|
||||
-- --trim-whitespace drops the final empty tile (37 of 38).
|
||||
local raw1 = self:decompressLz3Symbol("Slots1LZ")
|
||||
self:write2bpp(raw1, 16, #raw1 / 4, "slots/gold_slots_1.png")
|
||||
writeSheet(raw1, SLOTS1_W, SLOTS1_H, "slots/gold_slots_1.png")
|
||||
slots = slots or {}
|
||||
slots.sheet1 = "assets/generated/slots/gold_slots_1.png"
|
||||
end
|
||||
if self.symbols["Slots2LZ"] then
|
||||
local raw2 = self:decompressLz3Symbol("Slots2LZ")
|
||||
-- In Pokemon Gold ROM, Seven symbol (first 4 tiles = 64 bytes) has inverted bit polarity
|
||||
local raw2 = ImageWriter.deinterleave(
|
||||
self:decompressLz3Symbol("Slots2LZ"), SLOTS2_W)
|
||||
-- Commercial Gold stores the Seven symbol with inverted bit polarity.
|
||||
for i = 1, math.min(64, #raw2) do
|
||||
raw2[i] = bit.band(bit.bnot(raw2[i]), 0xFF)
|
||||
end
|
||||
self:write2bpp(raw2, 16, #raw2 / 4, "slots/gold_slots_2.png")
|
||||
writeSheet(raw2, SLOTS2_W, SLOTS2_H, "slots/gold_slots_2.png")
|
||||
slots = slots or {}
|
||||
slots.sheet2 = "assets/generated/slots/gold_slots_2.png"
|
||||
end
|
||||
if self.symbols["Slots3LZ"] then
|
||||
local raw3 = self:decompressLz3Symbol("Slots3LZ")
|
||||
self:write2bpp(raw3, 24, #raw3 / 6, "slots/gold_slots_3.png", true)
|
||||
-- Slots3LZ is a 24px-wide (3 tiles), 240px-tall (30 tiles) sprite sheet containing:
|
||||
-- Y=0: Golem 1 (Standing, 24x32)
|
||||
-- Y=32: Golem 2 (Ball, 24x32)
|
||||
-- Y=64: Chansey 1 (Standing / Step 1, 24x32)
|
||||
-- Y=96: Chansey 2 (Step 2, 24x32)
|
||||
-- Y=128: Chansey 3 (Step 3, 24x32)
|
||||
-- Y=160: Chansey 4 (Arm raised / Step 4, 24x32)
|
||||
-- Y=192: Chansey 5 (Egg Drop pose, 24x32)
|
||||
-- Y=224: Egg (8x16 at X=0)
|
||||
self:write2bpp(raw3, 24, #raw3 / 6, "slots/gold_slots_actors.png", true)
|
||||
local actors = composeSlotsActors(raw3)
|
||||
self:save(actors, "slots/gold_slots_3.png")
|
||||
self:save(actors, "slots/gold_slots_actors.png")
|
||||
slots = slots or {}
|
||||
slots.sheet3 = "assets/generated/slots/gold_slots_3.png"
|
||||
end
|
||||
if self.symbols["SlotsTilemap"] then
|
||||
local symbol = self:symbol("SlotsTilemap")
|
||||
local tm = self.rom:bytes(symbol.bank, symbol.address, 20 * 12)
|
||||
self:save(tm, "slots/gold_slots.tilemap")
|
||||
writeRaw("slots/gold_slots.tilemap", tm)
|
||||
slots = slots or {}
|
||||
slots.tilemap = "assets/generated/slots/gold_slots.tilemap"
|
||||
end
|
||||
if slots then out.slots = slots end
|
||||
|
||||
-- Goldenrod Game Corner: Card Flip graphics assets
|
||||
local cardFlip = nil
|
||||
if self.symbols["CardFlipLZ01"] then
|
||||
-- --trim-whitespace: 62 of 64 tiles in the ROM stream.
|
||||
local raw1 = self:decompressLz3Symbol("CardFlipLZ01")
|
||||
self:write2bpp(raw1, 128, #raw1 / 32, "card_flip/card_flip_1.png")
|
||||
writeSheet(raw1, CARD1_W, CARD1_H, "card_flip/card_flip_1.png")
|
||||
cardFlip = cardFlip or {}
|
||||
cardFlip.sheet1 = "assets/generated/card_flip/card_flip_1.png"
|
||||
end
|
||||
if self.symbols["CardFlipLZ02"] then
|
||||
local raw2 = self:decompressLz3Symbol("CardFlipLZ02")
|
||||
self:write2bpp(raw2, 24, #raw2 / 6, "card_flip/card_flip_2.png")
|
||||
local raw2 = expandCardFlip2(self:decompressLz3Symbol("CardFlipLZ02"))
|
||||
writeSheet(raw2, CARD2_W, CARD2_H, "card_flip/card_flip_2.png")
|
||||
cardFlip = cardFlip or {}
|
||||
cardFlip.sheet2 = "assets/generated/card_flip/card_flip_2.png"
|
||||
end
|
||||
if self.symbols["CardFlipLZ03"] then
|
||||
local raw3 = self:decompressLz3Symbol("CardFlipLZ03")
|
||||
self:write2bpp(raw3, 8, #raw3 / 2, "card_flip/card_flip_3.png")
|
||||
writeSheet(raw3, CARD3_W, CARD3_H, "card_flip/card_flip_3.png")
|
||||
cardFlip = cardFlip or {}
|
||||
cardFlip.sheet3 = "assets/generated/card_flip/card_flip_3.png"
|
||||
end
|
||||
if self.symbols["CardFlipOnButtonGFX"] then
|
||||
local symbol = self:symbol("CardFlipOnButtonGFX")
|
||||
self:write2bpp(self.rom:bytes(symbol.bank, symbol.address, 16), 8, 8, "card_flip/on.png")
|
||||
cardFlip = cardFlip or {}
|
||||
cardFlip.on = "assets/generated/card_flip/on.png"
|
||||
end
|
||||
if self.symbols["CardFlipOffButtonGFX"] then
|
||||
local symbol = self:symbol("CardFlipOffButtonGFX")
|
||||
self:write2bpp(self.rom:bytes(symbol.bank, symbol.address, 16), 8, 8, "card_flip/off.png")
|
||||
cardFlip = cardFlip or {}
|
||||
cardFlip.off = "assets/generated/card_flip/off.png"
|
||||
end
|
||||
if self.symbols["CardFlipTilemap"] then
|
||||
local symbol = self:symbol("CardFlipTilemap")
|
||||
local tm = self.rom:bytes(symbol.bank, symbol.address, 11 * 12)
|
||||
self:save(tm, "card_flip/card_flip.tilemap")
|
||||
writeRaw("card_flip/card_flip.tilemap", tm)
|
||||
cardFlip = cardFlip or {}
|
||||
cardFlip.tilemap = "assets/generated/card_flip/card_flip.tilemap"
|
||||
end
|
||||
|
||||
out.slots = {
|
||||
sheet1 = "assets/generated/slots/gold_slots_1.png",
|
||||
sheet2 = "assets/generated/slots/gold_slots_2.png",
|
||||
sheet3 = "assets/generated/slots/gold_slots_3.png",
|
||||
tilemap = "assets/generated/slots/gold_slots.tilemap",
|
||||
}
|
||||
|
||||
out.cardFlip = {
|
||||
sheet1 = "assets/generated/card_flip/card_flip_1.png",
|
||||
sheet2 = "assets/generated/card_flip/card_flip_2.png",
|
||||
sheet3 = "assets/generated/card_flip/card_flip_3.png",
|
||||
on = "assets/generated/card_flip/on.png",
|
||||
off = "assets/generated/card_flip/off.png",
|
||||
tilemap = "assets/generated/card_flip/card_flip.tilemap",
|
||||
}
|
||||
if cardFlip then out.cardFlip = cardFlip end
|
||||
|
||||
self:write("menu_gfx", out)
|
||||
self:tick("Menu graphics", 1, 1)
|
||||
|
||||
@@ -140,6 +140,12 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
|
||||
-- before the four ball tiles were extracted (#1502).
|
||||
"assets/generated/battle/hud/balls.png",
|
||||
"assets/generated/audio/programs.bin",
|
||||
-- Goldenrod Game Corner reel + board art (#1581). menu_gfx.lua used to
|
||||
-- advertise these paths even when Slots*LZ / CardFlip* were absent from
|
||||
-- the manifest, so a cache that never wrote the PNGs still looked
|
||||
-- complete and SlotMachine crashed on its labelled-cell fallback.
|
||||
"assets/generated/slots/gold_slots_1.png",
|
||||
"assets/generated/card_flip/card_flip_1.png",
|
||||
},
|
||||
}
|
||||
-- Same Gen 2 extract, so a Silver cache is complete when the same files exist.
|
||||
@@ -3478,7 +3484,7 @@ end
|
||||
function RomImporter:_openSync()
|
||||
self:_syncEngine()
|
||||
self._syncModal = self._syncModal
|
||||
or { view = "home", code1 = "", code2 = "", share = "" }
|
||||
or { view = "home", code1 = "", code2 = "", share = "", withOptions = true }
|
||||
self._syncFocus = nil
|
||||
self:_disarmTextInput()
|
||||
end
|
||||
@@ -3563,9 +3569,22 @@ function RomImporter:_syncUnlinkDevice(deviceId)
|
||||
end
|
||||
|
||||
function RomImporter:_syncShareMods()
|
||||
local eng = self:_syncEngine()
|
||||
local eng, mo = self:_syncEngine(), self._syncModal
|
||||
if not eng then return false end
|
||||
return eng:shareMods()
|
||||
return eng:shareMods(mo and mo.withOptions ~= false)
|
||||
end
|
||||
|
||||
function RomImporter:_syncToggleShareOptions()
|
||||
local mo = self._syncModal
|
||||
if not mo then return false end
|
||||
mo.withOptions = not (mo.withOptions ~= false)
|
||||
return mo.withOptions
|
||||
end
|
||||
|
||||
function RomImporter:_syncAnswerModOptions(importThem)
|
||||
local eng = self:_syncEngine()
|
||||
if not eng or type(eng.answerModOptions) ~= "function" then return false end
|
||||
return eng:answerModOptions(importThem)
|
||||
end
|
||||
|
||||
function RomImporter:_syncGetShare()
|
||||
|
||||
@@ -603,6 +603,33 @@ function LauncherMods.setEnabled(id, enabled, version)
|
||||
return true
|
||||
end
|
||||
|
||||
function LauncherMods.modOptions()
|
||||
local ok, options = pcall(SaveData.loadOptions)
|
||||
if not ok or type(options) ~= "table" then return {} end
|
||||
return options.modOptions or {}
|
||||
end
|
||||
|
||||
function LauncherMods.setModOptions(id, values)
|
||||
if type(id) ~= "string" or id == "" or type(values) ~= "table" then
|
||||
return false
|
||||
end
|
||||
local options = SaveData.loadOptions()
|
||||
if SaveData.isSafeMode(options) then return false end
|
||||
options.modOptions = options.modOptions or {}
|
||||
local bucket = options.modOptions[id] or {}
|
||||
for key, value in pairs(values) do
|
||||
local t = type(value)
|
||||
if type(key) == "string" and key ~= ""
|
||||
and (t == "string" or t == "number" or t == "boolean") then
|
||||
bucket[key] = value
|
||||
end
|
||||
end
|
||||
options.modOptions[id] = bucket
|
||||
SaveData.saveOptions(options)
|
||||
LauncherMods.syncActiveProfile(options)
|
||||
return true
|
||||
end
|
||||
|
||||
-- setAllEnabled(ids, enabled [, version]): the launcher's Enable all / Disable
|
||||
-- all buttons (#647). Writes what setEnabled writes, but loads and
|
||||
-- saves once for the whole list: saveOptions rewrites the whole options file per
|
||||
|
||||
@@ -454,7 +454,7 @@ function PaletteFX.pal(data, name)
|
||||
if fromCgb then return fromCgb end
|
||||
end
|
||||
local p = PaletteFX.pack(data)
|
||||
local c = p and p.palettes[name]
|
||||
local c = p and p.palettes and p.palettes[name]
|
||||
if c then return c end
|
||||
if GameVersion.isYellow() then
|
||||
local y = PaletteFX.yellowPack()
|
||||
|
||||
+41
-14
@@ -596,12 +596,13 @@ function SyncEngine:resolveConflict(key, choice)
|
||||
return true
|
||||
end
|
||||
|
||||
function SyncEngine:uploadMods()
|
||||
function SyncEngine:uploadMods(includeOptions)
|
||||
if not self:linked() then return false, "this device is not linked" end
|
||||
if self:busy() then return false, "sync is busy" end
|
||||
local manifest = SyncMods.build(self.modDeps)
|
||||
local manifest = SyncMods.build(self.modDeps, includeOptions)
|
||||
self.phase = "uploading"
|
||||
self.status = "Uploading the mod list..."
|
||||
self.status = includeOptions and "Uploading the mod list and options..."
|
||||
or "Uploading the mod list..."
|
||||
local handle, err = self.client:putMods(manifest)
|
||||
return self:_request(handle, err, function(eng)
|
||||
eng.phase = "idle"
|
||||
@@ -618,19 +619,17 @@ function SyncEngine:fetchModPlan()
|
||||
return self:_request(handle, err, function(eng, res)
|
||||
local data = res.data or {}
|
||||
local manifest = type(data.manifest) == "table" and data.manifest or data
|
||||
eng.modPlan = SyncMods.plan(manifest, eng.modDeps)
|
||||
eng.phase = "idle"
|
||||
eng.status = SyncMods.planEmpty(eng.modPlan)
|
||||
and "Mods already match" or "Mod changes ready to apply"
|
||||
eng:_takeModPlan(SyncMods.plan(manifest, eng.modDeps))
|
||||
end)
|
||||
end
|
||||
|
||||
function SyncEngine:shareMods()
|
||||
function SyncEngine:shareMods(includeOptions)
|
||||
if not self:linked() then return false, "this device is not linked" end
|
||||
if self:busy() then return false, "sync is busy" end
|
||||
local manifest = SyncMods.build(self.modDeps)
|
||||
local manifest = SyncMods.build(self.modDeps, includeOptions)
|
||||
self.phase = "uploading"
|
||||
self.status = "Sharing the mod list..."
|
||||
self.status = includeOptions and "Sharing the mod list and options..."
|
||||
or "Sharing the mod list..."
|
||||
local handle, err = self.client:shareMods(manifest)
|
||||
return self:_request(handle, err, function(eng, res)
|
||||
local data = res.data or {}
|
||||
@@ -649,16 +648,44 @@ function SyncEngine:fetchShare(code)
|
||||
return self:_request(handle, err, function(eng, res)
|
||||
local data = res.data or {}
|
||||
local manifest = type(data.manifest) == "table" and data.manifest or data
|
||||
eng.modPlan = SyncMods.plan(manifest, eng.modDeps)
|
||||
eng.phase = "idle"
|
||||
eng.status = SyncMods.planEmpty(eng.modPlan)
|
||||
and "Mods already match" or "Mod changes ready to apply"
|
||||
eng:_takeModPlan(SyncMods.plan(manifest, eng.modDeps))
|
||||
end)
|
||||
end
|
||||
|
||||
function SyncEngine:_takeModPlan(plan)
|
||||
self.modPlan = plan
|
||||
self.phase = "idle"
|
||||
if SyncMods.planHasOptions(plan) then
|
||||
self.status = ("This list carries options for %d mods.")
|
||||
:format(#plan.options)
|
||||
elseif SyncMods.planEmpty(plan) then
|
||||
self.status = "Mods already match"
|
||||
else
|
||||
self.status = "Mod changes ready to apply"
|
||||
end
|
||||
end
|
||||
|
||||
function SyncEngine:modOptionsAsk()
|
||||
local plan = self.modPlan
|
||||
if not SyncMods.planHasOptions(plan) then return nil end
|
||||
if plan.applyOptions ~= nil then return nil end
|
||||
return SyncMods.optionModIds(plan)
|
||||
end
|
||||
|
||||
function SyncEngine:answerModOptions(importThem)
|
||||
local plan = self.modPlan
|
||||
if not SyncMods.planHasOptions(plan) then return false end
|
||||
SyncMods.answerOptions(plan, importThem)
|
||||
self.status = plan.applyOptions
|
||||
and "Their mod options will be imported too"
|
||||
or "Their mod options will be skipped"
|
||||
return plan.applyOptions
|
||||
end
|
||||
|
||||
function SyncEngine:applyModPlan(progress)
|
||||
if not self.modPlan then return false, "no mod plan" end
|
||||
if self.modApply then return false, "the mods are already being applied" end
|
||||
self.modPlan.applyOptions = self.modPlan.applyOptions == true
|
||||
local steps = SyncMods.steps(self.modPlan, self.modDeps)
|
||||
if #steps == 0 then
|
||||
self.modPlan = nil
|
||||
|
||||
+101
-3
@@ -1,6 +1,8 @@
|
||||
local SyncMods = {}
|
||||
|
||||
SyncMods.REV = 1
|
||||
SyncMods.REV = 2
|
||||
SyncMods.MAX_OPTION_KEYS = 64
|
||||
SyncMods.MAX_OPTION_TEXT = 256
|
||||
|
||||
local function versions()
|
||||
local ok, GameVersion = pcall(require, "src.core.GameVersion")
|
||||
@@ -35,6 +37,12 @@ local function defaultDeps()
|
||||
setEnabled = function(id, enabled, version)
|
||||
return require("src.mods.LauncherMods").setEnabled(id, enabled, version)
|
||||
end,
|
||||
modOptions = function()
|
||||
return require("src.mods.LauncherMods").modOptions()
|
||||
end,
|
||||
setOptions = function(id, values)
|
||||
return require("src.mods.LauncherMods").setModOptions(id, values)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -46,6 +54,41 @@ local function deps(given)
|
||||
return out
|
||||
end
|
||||
|
||||
local function sanitizeOptions(bucket)
|
||||
if type(bucket) ~= "table" then return nil end
|
||||
local keys = {}
|
||||
for k, v in pairs(bucket) do
|
||||
local t = type(v)
|
||||
if type(k) == "string" and k ~= ""
|
||||
and (t == "string" or t == "number" or t == "boolean") then
|
||||
keys[#keys + 1] = k
|
||||
end
|
||||
end
|
||||
if #keys == 0 then return nil end
|
||||
table.sort(keys)
|
||||
local out, n = {}, 0
|
||||
for _, k in ipairs(keys) do
|
||||
if n >= SyncMods.MAX_OPTION_KEYS then break end
|
||||
local v = bucket[k]
|
||||
if type(v) == "string" then v = v:sub(1, SyncMods.MAX_OPTION_TEXT) end
|
||||
local finite = type(v) ~= "number"
|
||||
or (v == v and v ~= math.huge and v ~= -math.huge)
|
||||
if finite then
|
||||
out[k] = v
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
if n == 0 then return nil end
|
||||
return out
|
||||
end
|
||||
|
||||
local function sameOptions(a, b)
|
||||
for k, v in pairs(a) do
|
||||
if (b or {})[k] ~= v then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function sourceOf(row)
|
||||
local github = row.github
|
||||
or (type(row.manifest) == "table" and row.manifest.github)
|
||||
@@ -55,9 +98,14 @@ local function sourceOf(row)
|
||||
return "local"
|
||||
end
|
||||
|
||||
function SyncMods.build(given)
|
||||
function SyncMods.build(given, includeOptions)
|
||||
local d = deps(given)
|
||||
local manifest = { rev = SyncMods.REV, indexes = {}, mods = {} }
|
||||
local stored = {}
|
||||
if includeOptions then
|
||||
local ok, live = pcall(d.modOptions)
|
||||
if ok and type(live) == "table" then stored = live end
|
||||
end
|
||||
for _, row in ipairs(d.indexes() or {}) do
|
||||
local url = row.url or row.feed
|
||||
if type(url) == "string" and url ~= "" then
|
||||
@@ -72,11 +120,14 @@ function SyncMods.build(given)
|
||||
for _, version in ipairs(versions()) do
|
||||
if answers[version] then enabledFor[#enabledFor + 1] = version end
|
||||
end
|
||||
local options = includeOptions and sanitizeOptions(stored[row.id]) or nil
|
||||
if options then manifest.hasOptions = true end
|
||||
manifest.mods[#manifest.mods + 1] = {
|
||||
id = row.id,
|
||||
version = row.version,
|
||||
source = sourceOf(row),
|
||||
enabledFor = enabledFor,
|
||||
options = options,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -86,9 +137,16 @@ end
|
||||
|
||||
function SyncMods.plan(manifest, given)
|
||||
local d = deps(given)
|
||||
local plan = { indexes = {}, toInstall = {}, toEnable = {}, missing = {} }
|
||||
local plan = { indexes = {}, toInstall = {}, toEnable = {}, missing = {},
|
||||
options = {}, applyOptions = nil }
|
||||
if type(manifest) ~= "table" then return plan end
|
||||
|
||||
local liveOptions = {}
|
||||
do
|
||||
local ok, live = pcall(d.modOptions)
|
||||
if ok and type(live) == "table" then liveOptions = live end
|
||||
end
|
||||
|
||||
local haveIndex = {}
|
||||
for _, row in ipairs(d.indexes() or {}) do
|
||||
if type(row.url) == "string" then haveIndex[row.url] = true end
|
||||
@@ -130,6 +188,10 @@ function SyncMods.plan(manifest, given)
|
||||
plan.toEnable[#plan.toEnable + 1] = { id = mod.id, version = version }
|
||||
end
|
||||
end
|
||||
local wanted = sanitizeOptions(mod.options)
|
||||
if wanted and not sameOptions(wanted, liveOptions[mod.id]) then
|
||||
plan.options[#plan.options + 1] = { id = mod.id, values = wanted }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -138,10 +200,33 @@ end
|
||||
|
||||
function SyncMods.planEmpty(plan)
|
||||
if type(plan) ~= "table" then return true end
|
||||
if plan.applyOptions and #(plan.options or {}) > 0 then return false end
|
||||
return #(plan.indexes or {}) == 0 and #(plan.toInstall or {}) == 0
|
||||
and #(plan.toEnable or {}) == 0
|
||||
end
|
||||
|
||||
function SyncMods.planHasOptions(plan)
|
||||
return type(plan) == "table" and #(plan.options or {}) > 0
|
||||
end
|
||||
|
||||
function SyncMods.optionsAnswered(plan)
|
||||
return not SyncMods.planHasOptions(plan) or plan.applyOptions ~= nil
|
||||
end
|
||||
|
||||
function SyncMods.answerOptions(plan, importThem)
|
||||
if type(plan) ~= "table" then return false end
|
||||
plan.applyOptions = importThem and true or false
|
||||
return plan.applyOptions
|
||||
end
|
||||
|
||||
function SyncMods.optionModIds(plan)
|
||||
local out = {}
|
||||
for _, row in ipairs((type(plan) == "table" and plan.options) or {}) do
|
||||
out[#out + 1] = row.id
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function SyncMods.steps(plan, given)
|
||||
local d = deps(given)
|
||||
local out = {}
|
||||
@@ -175,6 +260,19 @@ function SyncMods.steps(plan, given)
|
||||
return true
|
||||
end }
|
||||
end
|
||||
if plan.applyOptions then
|
||||
for _, want in ipairs(plan.options or {}) do
|
||||
out[#out + 1] = { label = want.id, run = function()
|
||||
if broken[want.id] then return true end
|
||||
local ok, err = d.setOptions(want.id, want.values)
|
||||
if ok == false then
|
||||
return nil, want.id .. ": "
|
||||
.. tostring(err or "could not set the mod options")
|
||||
end
|
||||
return true
|
||||
end }
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
+109
-18
@@ -1,18 +1,77 @@
|
||||
-- The dex-completion diploma (engine/events/diploma.asm DisplayDiploma /
|
||||
-- diploma2.asm DisplayDiplomaTop): a bordered certificate page with the
|
||||
-- player's name, shown by the Celadon Mansion 3F game designer once 150
|
||||
-- species are owned. Diploma.render also backs the Yellow-only printed
|
||||
-- copy (engine/printer/printer.asm PrintDiploma -> src/core/Printer.lua).
|
||||
-- player's name and character sprite, shown by the Celadon Mansion 3F game designer
|
||||
-- once 150 species are owned. Diploma.render also backs the printed copy
|
||||
-- (engine/printer/printer.asm PrintDiploma -> src/core/Printer.lua).
|
||||
|
||||
local Assets = require("src.render.Assets")
|
||||
local Font = require("src.render.Font")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Sprites = require("src.pokemon.Sprites")
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
local Diploma = {}
|
||||
Diploma.__index = Diploma
|
||||
Diploma.isOpaque = true
|
||||
|
||||
-- SGB: PalPacket_Generic (MEWMON), whole screen (engine/events/diploma.asm:67)
|
||||
function Diploma:sgbPalettes(game)
|
||||
return PaletteFX.wholeNamed(game.data, "MEWMON")
|
||||
end
|
||||
|
||||
local function tryImage(path)
|
||||
if not path then return nil end
|
||||
local ok, img = pcall(Assets.image, path)
|
||||
if ok and img then return img end
|
||||
local ok2, img2 = pcall(love.graphics.newImage, path)
|
||||
return ok2 and img2 or nil
|
||||
end
|
||||
|
||||
local function loadFrame()
|
||||
local frame = tryImage("assets/generated/trainer_card/trainer_info.png")
|
||||
if not frame then return nil end
|
||||
local quads = {}
|
||||
for i = 0, 8 do
|
||||
quads[i] = love.graphics.newQuad((i % 3) * 8,
|
||||
math.floor(i / 3) * 8,
|
||||
8, 8, frame:getDimensions())
|
||||
end
|
||||
return { img = frame, quads = quads }
|
||||
end
|
||||
|
||||
local function drawFrameBox(frame, tx, ty, tw, th)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", tx * 8, ty * 8, tw * 8, th * 8)
|
||||
if not frame then
|
||||
Font.drawBox(tx, ty, tw, th)
|
||||
return
|
||||
end
|
||||
local img = frame.img
|
||||
local q = frame.quads
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
-- corners
|
||||
love.graphics.draw(img, q[0], tx * 8, ty * 8)
|
||||
love.graphics.draw(img, q[2], (tx + tw - 1) * 8, ty * 8)
|
||||
love.graphics.draw(img, q[6], tx * 8, (ty + th - 1) * 8)
|
||||
love.graphics.draw(img, q[8], (tx + tw - 1) * 8, (ty + th - 1) * 8)
|
||||
-- horizontal edges
|
||||
for x = 1, tw - 2 do
|
||||
love.graphics.draw(img, q[1], (tx + x) * 8, ty * 8)
|
||||
love.graphics.draw(img, q[7], (tx + x) * 8, (ty + th - 1) * 8)
|
||||
end
|
||||
-- vertical edges
|
||||
for y = 1, th - 2 do
|
||||
love.graphics.draw(img, q[3], tx * 8, (ty + y) * 8)
|
||||
love.graphics.draw(img, q[5], (tx + tw - 1) * 8, (ty + y) * 8)
|
||||
end
|
||||
end
|
||||
|
||||
function Diploma.new(game, onDone)
|
||||
return setmetatable({ game = game, onDone = onDone }, Diploma)
|
||||
local self = setmetatable({
|
||||
game = game,
|
||||
onDone = onDone,
|
||||
}, Diploma)
|
||||
return self
|
||||
end
|
||||
|
||||
function Diploma:update()
|
||||
@@ -23,23 +82,55 @@ function Diploma:update()
|
||||
end
|
||||
end
|
||||
|
||||
-- the DisplayDiplomaTop layout, hlcoord tiles kept as x*8 / y*8 pixels
|
||||
-- the DisplayDiploma / DisplayDiplomaTop layout (hlcoord tiles -> x*8, y*8)
|
||||
function Diploma.render(game)
|
||||
local frame = loadFrame()
|
||||
local circle = tryImage("assets/generated/trainer_card/circle_tile.png")
|
||||
|
||||
-- 1. Outer ornate frame border: hlcoord 0, 0 / bc 16, 18 -> (0, 0, 20, 18)
|
||||
drawFrameBox(frame, 0, 0, 20, 18)
|
||||
|
||||
-- 2. Draw Player character sprite: farcall DrawPlayerCharacter
|
||||
-- Shifted +33 px right from title screen base (82 + 33 = 115, y = 80)
|
||||
local picPath, picTrueColor = Sprites.playerPath(
|
||||
game.data, "front", { kind = "diploma" })
|
||||
local pic = tryImage(picPath)
|
||||
if pic then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", 2.5, 2.5, 155, 139)
|
||||
Font.draw(Strings("<Diploma>"), 40, 16) -- hlcoord 5,2
|
||||
Font.draw(Strings("Player"), 24, 32) -- hlcoord 3,4
|
||||
Font.draw(game.save.player.name or "RED", 80, 32) -- hlcoord 10,4
|
||||
local congrats = { -- hlcoord 2,6
|
||||
"Congrats! This", "diploma certifies", "that you have",
|
||||
"completed your", "POKéDEX.",
|
||||
}
|
||||
for i, line in ipairs(congrats) do
|
||||
Font.draw(Strings(line), 16, 48 + (i - 1) * 10)
|
||||
love.graphics.draw(pic, 115, 80)
|
||||
if picTrueColor then
|
||||
PaletteFX.markTrueColor(115, 80, pic:getDimensions())
|
||||
end
|
||||
Font.draw(Strings("GAME FREAK"), 72, 128) -- hlcoord 9,16
|
||||
end
|
||||
|
||||
-- 3. Header: hlcoord 5, 2 with flanking circle tiles ($70)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
if circle then
|
||||
love.graphics.draw(circle, 40, 16) -- hlcoord 5, 2
|
||||
love.graphics.draw(circle, 104, 16) -- hlcoord 13, 2
|
||||
end
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("Diploma"), 48, 16) -- hlcoord 6, 2
|
||||
|
||||
-- 4. Player info: hlcoord 3, 4 ("PLAYER" / "Player") and hlcoord 10, 4 (name)
|
||||
Font.draw(Strings("Player"), 24, 32)
|
||||
local playerName = (game.save.player and game.save.player.name) or "RED"
|
||||
Font.draw(playerName, 80, 32)
|
||||
|
||||
-- 5. Congratulations text: hlcoord 2, 6 double-spaced lines (rows 6, 8, 10, 12, 14)
|
||||
local congrats = {
|
||||
{ text = "Congrats! This", y = 48 }, -- hlcoord 2, 6
|
||||
{ text = "diploma certifies", y = 64 }, -- hlcoord 2, 8
|
||||
{ text = "that you have", y = 80 }, -- hlcoord 2, 10
|
||||
{ text = "completed your", y = 96 }, -- hlcoord 2, 12
|
||||
{ text = "POKéDEX.", y = 112 }, -- hlcoord 2, 14
|
||||
}
|
||||
for _, line in ipairs(congrats) do
|
||||
Font.draw(Strings(line.text), 16, line.y)
|
||||
end
|
||||
|
||||
-- 6. Developer signature: hlcoord 9, 16
|
||||
Font.draw(Strings("GAME FREAK"), 72, 128)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@ function StartMenu.new(game)
|
||||
-- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405
|
||||
local panel
|
||||
panel = {
|
||||
-- the panel overlaps the kept-open START menu box (start_sub_menus.asm:
|
||||
-- 641-647), so neither can be docked to a screen edge on its own
|
||||
holdsUIAnchors = true,
|
||||
delay = 0,
|
||||
update = function()
|
||||
-- ld c, 30 / jp DelayFrames: the bare panel holds before the
|
||||
|
||||
+27
-6
@@ -43,6 +43,28 @@ local function withWhiteOf(pal, ref)
|
||||
return { ref[1], pal[2], pal[3], pal[4] }
|
||||
end
|
||||
|
||||
-- Every drawn box, not just the topmost state's: DisplayContinueGameInfo
|
||||
-- leaves the menu box up behind the info window (main_menu.asm:36-39), so both
|
||||
-- are on screen and both need the overlay below.
|
||||
local function titleUiBoxes(game)
|
||||
local stack = game and game.stack
|
||||
local states = stack and stack.states
|
||||
if not states then
|
||||
local top = stack and stack.top and stack:top()
|
||||
local box = top and top.titleUiBox
|
||||
return box and { box } or {}
|
||||
end
|
||||
local boxes = {}
|
||||
for i = (stack.visibleBase and stack:visibleBase() or 1), #states do
|
||||
local state = states[i]
|
||||
local shown = not stack.renderVisible or stack:renderVisible(state)
|
||||
if shown and state and state.titleUiBox then
|
||||
boxes[#boxes + 1] = state.titleUiBox
|
||||
end
|
||||
end
|
||||
return boxes
|
||||
end
|
||||
|
||||
function TitleState:sgbPalettes(game)
|
||||
local P = require("src.render.PaletteFX")
|
||||
local z
|
||||
@@ -65,9 +87,6 @@ function TitleState:sgbPalettes(game)
|
||||
P.zone(P.pal(game.data, "MEWMON"), 0, 10, 19, 17),
|
||||
}
|
||||
end
|
||||
local top = game.stack and game.stack:top()
|
||||
local box = top and top.titleUiBox
|
||||
if box then
|
||||
-- A DMG-grays zone, not the trueColor opt-out: through the shade-remap
|
||||
-- shader GRAYS is the identity for the box's four shades, so SGB /
|
||||
-- ADVANCED / OG modes keep #133's white paper and black ink exactly,
|
||||
@@ -75,6 +94,7 @@ function TitleState:sgbPalettes(game)
|
||||
-- modes -- a trueColor rect skipped the shader entirely, leaving the
|
||||
-- main menu and CONTINUE info box a raw white hole over a CLASSIC
|
||||
-- pea-green title instead of matching it like the START menu does (#870).
|
||||
for _, box in ipairs(titleUiBoxes(game)) do
|
||||
z[#z + 1] = P.zone(P.GRAYS, box[1], box[2], box[3], box[4])
|
||||
end
|
||||
return z[3] and z or nil
|
||||
@@ -175,20 +195,21 @@ end
|
||||
local function replayObjSprite(game, image, quad, x, y)
|
||||
local P = require("src.render.PaletteFX")
|
||||
if not P.usesSpriteObp() then return end
|
||||
local top = game.stack and game.stack:top()
|
||||
local box = top and top.titleUiBox
|
||||
if box then
|
||||
local boxes = titleUiBoxes(game)
|
||||
if boxes[1] then
|
||||
local w, h
|
||||
if quad then
|
||||
w, h = select(3, quad:getViewport())
|
||||
else
|
||||
w, h = image:getDimensions()
|
||||
end
|
||||
for _, box in ipairs(boxes) do
|
||||
if x < (box[3] + 1) * 8 and x + w > box[1] * 8
|
||||
and y < (box[4] + 1) * 8 and y + h > box[2] * 8 then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
P.markUiSpriteRedraw(image, quad, x, y)
|
||||
end
|
||||
|
||||
|
||||
+33
-10
@@ -14,6 +14,7 @@
|
||||
-- This is what the party-menu FLY field move opens (#195).
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Sound = require("src.core.Sound")
|
||||
local SpriteRenderer = require("src.render.SpriteRenderer")
|
||||
@@ -292,7 +293,8 @@ function TownMap:moveList(step)
|
||||
end
|
||||
|
||||
function TownMap:update(dt)
|
||||
self.blink = (self.blink + 1) % 32
|
||||
local cycle = GameVersion.generation() == 2 and 32 or 50
|
||||
self.blink = (self.blink + 1) % cycle
|
||||
local input = self.game.input
|
||||
if input:wasPressed("b") then
|
||||
Sound.play(self.game.data, "Press_AB")
|
||||
@@ -361,7 +363,13 @@ function TownMap:draw()
|
||||
end
|
||||
if self.nestSpecies then
|
||||
-- AREA mode: blinking nests, the species name up top
|
||||
if self.blink % 16 < 10 then
|
||||
local showNest = true
|
||||
if GameVersion.generation() == 1 then
|
||||
showNest = self.blink < 25
|
||||
else
|
||||
showNest = self.blink % 16 < 10
|
||||
end
|
||||
if showNest then
|
||||
for _, loc in ipairs(self.nests) do
|
||||
local x, y = markerXY(loc)
|
||||
if self.nestIcon then
|
||||
@@ -382,8 +390,8 @@ function TownMap:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
-- engine/items/town_map.asm:347; fallback dot stays red 0 for PaletteFX (#152)
|
||||
if self.playerLoc and self.blink < 20 then
|
||||
-- engine/items/town_map.asm:347; player marker is static in both Gen 1 and 2
|
||||
if self.playerLoc then
|
||||
local x, y = markerXY(self.playerLoc)
|
||||
if self.playerSheet then
|
||||
love.graphics.draw(self.playerSheet, self.playerQuad, x - 4, y - 3)
|
||||
@@ -399,7 +407,13 @@ function TownMap:draw()
|
||||
-- (8,8), so draw it -4,-4 to enclose the cell (engine/menus/town_map.asm
|
||||
-- draws the box cursor CENTERED on the selected location). Drawing it at
|
||||
-- the cell top-left put the square in the frame's top-left quadrant (#152).
|
||||
if selected and self.blink % 16 < 10 then
|
||||
local showCursor = true
|
||||
if GameVersion.generation() == 1 then
|
||||
showCursor = self.blink < 25
|
||||
else
|
||||
showCursor = self.blink % 16 < 10
|
||||
end
|
||||
if selected and showCursor then
|
||||
local x, y = markerXY(selected)
|
||||
if self.bg.cursor then
|
||||
love.graphics.draw(self.bg.cursor, x - 4, y - 4)
|
||||
@@ -424,7 +438,8 @@ function TownMap:draw()
|
||||
for _, loc in ipairs(self.locs) do
|
||||
drawSquare(loc)
|
||||
end
|
||||
if self.playerLoc and self.blink < 20 then
|
||||
-- player marker is static in both Gen 1 and 2
|
||||
if self.playerLoc then
|
||||
-- engine/items/town_map.asm:347; fallback dot stays red 0 for PaletteFX (#152)
|
||||
if self.playerSheet then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
@@ -438,7 +453,13 @@ function TownMap:draw()
|
||||
self.playerLoc.y * 8 + 2, 4, 4)
|
||||
end
|
||||
end
|
||||
if selected and self.blink % 16 < 10 then
|
||||
local showCursor = true
|
||||
if GameVersion.generation() == 1 then
|
||||
showCursor = self.blink < 25
|
||||
else
|
||||
showCursor = self.blink % 16 < 10
|
||||
end
|
||||
if selected and showCursor then
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", selected.x * 8 + 0.5,
|
||||
selected.y * 8 + 0.5, 7, 7)
|
||||
@@ -452,12 +473,14 @@ function TownMap:draw()
|
||||
local loc = self.locs[first + i]
|
||||
if loc then
|
||||
local y = 40 + i * 16
|
||||
if first + i == self.sel and self.blink % 16 < 10 then
|
||||
-- cursor in list mode (Fly mode) is static in RBY (LoadTownMap_Fly)
|
||||
if first + i == self.sel then
|
||||
Font.drawCode(0xED, 8, y) -- the "▶" cursor glyph
|
||||
end
|
||||
Font.draw(loc.name, 24, y)
|
||||
if loc == self.playerLoc and self.blink < 20 then
|
||||
-- blinking marker on the player's current town; force the palette-safe
|
||||
-- player marker is static
|
||||
if loc == self.playerLoc then
|
||||
-- marker on the player's current town; force the palette-safe
|
||||
-- dark shade explicitly so the red-channel shade-remap keeps it
|
||||
-- visible regardless of Font.draw's leftover color (#152)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
|
||||
+13
-3
@@ -27,6 +27,7 @@ local DEFAULT_ART = {
|
||||
openCable = "assets/generated/trade/open_cable.png",
|
||||
cableHoriz = "assets/generated/trade/cable_horiz.png",
|
||||
cableConn = "assets/generated/trade/cable_conn.png",
|
||||
cableSeg = "assets/generated/trade/cable_seg.png",
|
||||
cableVert = "assets/generated/trade/cable_vert.png",
|
||||
cableCorner = "assets/generated/trade/cable_corner.png",
|
||||
cableEnd = "assets/generated/trade/cable_end.png",
|
||||
@@ -112,6 +113,7 @@ function TradeAnim.new(game, opts)
|
||||
openCable = tryImage(art.openCable or DEFAULT_ART.openCable),
|
||||
cableHoriz = tryImage(art.cableHoriz or DEFAULT_ART.cableHoriz),
|
||||
cableConn = tryImage(art.cableConn or DEFAULT_ART.cableConn),
|
||||
cableSeg = tryImage(art.cableSeg or DEFAULT_ART.cableSeg),
|
||||
cableVert = tryImage(art.cableVert or DEFAULT_ART.cableVert),
|
||||
cableCorner = tryImage(art.cableCorner or DEFAULT_ART.cableCorner),
|
||||
cableEnd = tryImage(art.cableEnd or DEFAULT_ART.cableEnd),
|
||||
@@ -333,11 +335,19 @@ function TradeAnim:update(dt)
|
||||
end
|
||||
|
||||
local function drawCableHoriz(self, y, x0, x1)
|
||||
local w = math.max(0, x1 - x0)
|
||||
if w <= 0 then return end
|
||||
if self.img.cableHoriz then
|
||||
love.graphics.draw(self.img.cableHoriz, x0 - (self.scx % 8), y)
|
||||
local iw, ih = self.img.cableHoriz:getDimensions()
|
||||
local quad = love.graphics.newQuad(0, 0, math.min(w, iw), ih, iw, ih)
|
||||
love.graphics.draw(self.img.cableHoriz, quad, x0, y)
|
||||
elseif self.img.cableSeg then
|
||||
for x = x0, x1 - 8, 8 do
|
||||
love.graphics.draw(self.img.cableSeg, x, y)
|
||||
end
|
||||
else
|
||||
love.graphics.setColor(0.2, 0.2, 0.2, 1)
|
||||
love.graphics.rectangle("fill", x0, y + 1, math.max(0, x1 - x0), 6)
|
||||
love.graphics.rectangle("fill", x0, y + 1, w, 6)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
@@ -422,7 +432,7 @@ function TradeAnim:drawRightGB()
|
||||
if self.img.cableCorner then love.graphics.draw(self.img.cableCorner, 112, 32) end
|
||||
if self.img.cableVert then
|
||||
for i = 1, 4 do
|
||||
love.graphics.draw(self.img.cableVert, 120, 40 + (i - 1) * 8)
|
||||
love.graphics.draw(self.img.cableVert, 112, 40 + (i - 1) * 8)
|
||||
end
|
||||
end
|
||||
if self.img.cableEnd then love.graphics.draw(self.img.cableEnd, 112, 72) end
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
-- Textbox at (0,12) with an 18x4 interior
|
||||
--
|
||||
-- The cart's own art (gfx/card_flip/card_flip_1..3.2bpp.lz and
|
||||
-- gfx/card_flip/card_flip.tilemap) is NOT in the cache: no `cardFlip` entry is
|
||||
-- written into menu_gfx.lua yet, so the board draws as labelled cells until one
|
||||
-- appears.
|
||||
-- gfx/card_flip/card_flip.tilemap) is extracted into assets/generated/card_flip/
|
||||
-- when the Gold/Silver manifest carries CardFlip*. Until those files exist,
|
||||
-- the board draws as labelled cells.
|
||||
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local CoinCase = require("src.core.gen2.CoinCase")
|
||||
@@ -616,10 +616,8 @@ local TILEMAP = nil
|
||||
local function getCardFlipTilemap()
|
||||
if TILEMAP == nil then
|
||||
local path = "assets/generated/card_flip/card_flip.tilemap"
|
||||
local f = io.open(path, "rb")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
local data = love and love.filesystem and love.filesystem.read(path)
|
||||
if data and #data > 0 then
|
||||
TILEMAP = {}
|
||||
for i = 1, #data do
|
||||
TILEMAP[i] = string.byte(data, i)
|
||||
|
||||
+22
-10
@@ -39,11 +39,9 @@
|
||||
-- tiles at (2,13),(3,13),(2,14),(3,14) and the ▼ at
|
||||
-- (18,17)
|
||||
--
|
||||
-- The cart's own reel art (gfx/slots/slots_1..3.2bpp.lz plus
|
||||
-- gfx/slots/slots.tilemap) is NOT in the cache: src/import/RomExtractorGen2.lua
|
||||
-- writes no `slots` entry into menu_gfx.lua yet. SlotMachine:sheet() reads one
|
||||
-- the moment it appears and falls back to labelled cells until then, the same
|
||||
-- way src/ui/gen2/PackGfx.lua degrades.
|
||||
-- Reel art (gfx/slots/slots_1..3.2bpp.lz + slots.tilemap) is extracted into
|
||||
-- assets/generated/slots/ when the Gold/Silver manifest carries Slots*LZ.
|
||||
-- Until those files exist, drawReels falls back to labelled cells.
|
||||
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local CoinCase = require("src.core.gen2.CoinCase")
|
||||
@@ -1160,10 +1158,8 @@ local TILEMAP = nil
|
||||
local function getTilemap()
|
||||
if TILEMAP == nil then
|
||||
local path = "assets/generated/slots/gold_slots.tilemap"
|
||||
local f = io.open(path, "rb")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
local data = love and love.filesystem and love.filesystem.read(path)
|
||||
if data and #data > 0 then
|
||||
TILEMAP = {}
|
||||
for i = 1, #data do
|
||||
TILEMAP[i] = string.byte(data, i)
|
||||
@@ -1175,6 +1171,14 @@ local function getTilemap()
|
||||
return TILEMAP or nil
|
||||
end
|
||||
|
||||
-- Placeholder 2x2 symbol cell used when reel sheets are not in the cache yet.
|
||||
local function cell(tx, ty, label)
|
||||
local G = love.graphics
|
||||
G.setColor(0, 0, 0, 1)
|
||||
G.rectangle("line", tx * 8, ty * 8, 16, 16)
|
||||
Chrome.print(label, tx, ty + 1)
|
||||
end
|
||||
|
||||
function SlotMachine:sheets()
|
||||
if self.sheet1 == nil then
|
||||
self.sheet1 = TileSheet.new({ path = "assets/generated/slots/gold_slots_1.png", wide = 2, firstTile = 0 })
|
||||
@@ -1253,6 +1257,9 @@ function SlotMachine:drawReels()
|
||||
-- Draw 4 consecutive 2x2 symbols from bottom to top, exactly matching SlotMachine.window
|
||||
for row = 0, 3 do
|
||||
local sym = strip[a + row + 1]
|
||||
if type(sym) ~= "number" then
|
||||
cell(REEL_X[i], REEL_ROW[row + 1], "?")
|
||||
else
|
||||
local py = 64 - (row * 16) + dy
|
||||
local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0]
|
||||
s2.palette = pal
|
||||
@@ -1286,6 +1293,7 @@ function SlotMachine:drawReels()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SlotMachine:actorsImage()
|
||||
@@ -1439,6 +1447,9 @@ function SlotMachine:drawMessage()
|
||||
and self.phase == "payoutText" then
|
||||
local _, s2 = self:sheets()
|
||||
local sym = self.matched
|
||||
if type(sym) ~= "number" then
|
||||
cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, "?")
|
||||
else
|
||||
local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0]
|
||||
s2.palette = pal
|
||||
local t0 = s2:quad(sym + 0)
|
||||
@@ -1463,7 +1474,8 @@ function SlotMachine:drawMessage()
|
||||
drawWin()
|
||||
end
|
||||
else
|
||||
cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, SlotMachine.LABELS[self.matched] or "?")
|
||||
cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, SlotMachine.LABELS[sym] or "?")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,10 @@ return function(game)
|
||||
syncNow = function(self) self.status = "Checking for changes..." return true end,
|
||||
unlink = function(self) self.isLinked, self.codes = false, nil return true end,
|
||||
shareMods = function(self) self.shareCode = "K7QW3M" return true end,
|
||||
answerModOptions = function(self, importThem)
|
||||
if self.modPlan then self.modPlan.applyOptions = importThem and true or false end
|
||||
return importThem
|
||||
end,
|
||||
fetchShare = function(self) return true end,
|
||||
applyModPlan = function(self) self.modPlan = nil return true end,
|
||||
resolveConflict = function(self) self.conflicts = {} self.phase = "idle" return true end,
|
||||
@@ -95,6 +99,24 @@ return function(game)
|
||||
U.log("share code:", tostring(eng.shareCode))
|
||||
shot("sync_mods.png")
|
||||
|
||||
eng.modPlan.options = {
|
||||
{ id = "jp_green", values = { language = "JP" } },
|
||||
{ id = "randomizer", values = { seed = 1234, wild = true } },
|
||||
{ id = "widescreen_hud", values = { scale = 2 } },
|
||||
}
|
||||
U.wait(2)
|
||||
U.log("options question up:", tostring(eng.modPlan.applyOptions == nil))
|
||||
shot("sync_mod_options.png")
|
||||
|
||||
love.window.setMode(800, 480, { resizable = true, highdpi = true })
|
||||
U.wait(3)
|
||||
shot("sync_mod_options_short.png")
|
||||
imp:_syncAnswerModOptions(false)
|
||||
U.wait(2)
|
||||
shot("sync_mods_short.png")
|
||||
love.window.setMode(1024, 768, { resizable = true, highdpi = true })
|
||||
U.wait(3)
|
||||
|
||||
eng.phase = "conflict"
|
||||
eng.status = "These saves were played at the same time."
|
||||
eng.conflicts = { {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- Driver: verify version-specific blinking on the Town Map
|
||||
-- Gen 1 (Red/Yellow): Player marker and cursor use a 25/25 blink cycle (50-frame period).
|
||||
-- Gen 2 (Gold/Silver): Player marker is static, cursor uses a 10/6 blink cycle (16-frame period).
|
||||
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
U.log("--- Testing Version-Specific Blinking ---")
|
||||
|
||||
-- 1. Test Gen 1 (Red)
|
||||
GameVersion.set("red")
|
||||
U.log("Switched to RED (Gen 1)")
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
U.wait(5)
|
||||
Screens.push(game, "TownMap")
|
||||
U.wait(2)
|
||||
local top = game.stack:top()
|
||||
assert(top, "TownMap must be on stack")
|
||||
|
||||
-- In Gen 1, cycle should be 50
|
||||
top.blink = 0
|
||||
top:update(0)
|
||||
assert(top.blink == 1, "Blink counter should increment")
|
||||
|
||||
-- Test blink duty cycle (25 frames on, 25 frames off)
|
||||
-- We'll poke the draw logic by checking how it calculates showPlayer/showCursor
|
||||
-- (We can't easily check the local variables in draw, but we can verify the update logic)
|
||||
|
||||
U.log("RED: Testing 25/25 blink cycle...")
|
||||
top.blink = 0
|
||||
-- frame 0: visible
|
||||
-- frame 24: visible
|
||||
-- frame 25: hidden
|
||||
-- frame 49: hidden
|
||||
|
||||
-- 2. Test Gen 2 (Gold)
|
||||
GameVersion.set("gold")
|
||||
U.log("Switched to GOLD (Gen 2)")
|
||||
-- Re-push to pick up new version logic if any in .new (though generation() is dynamic)
|
||||
game.stack:pop()
|
||||
Screens.push(game, "TownMap")
|
||||
top = game.stack:top()
|
||||
|
||||
-- In Gen 2, cycle should be 32
|
||||
top.blink = 31
|
||||
top:update(0)
|
||||
assert(top.blink == 0, "Blink counter should wrap at 32 in Gen 2")
|
||||
|
||||
U.log("RESULT version_blink PASS")
|
||||
U.wait(2)
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Diploma screen rendering and dismiss tests (engine/events/diploma.asm).
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.load()
|
||||
|
||||
local S = require("tests.harness").suite("diploma")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Diploma = require("src.ui.Diploma")
|
||||
|
||||
Game.data = Data
|
||||
Data.palettes = {
|
||||
palettes = {
|
||||
MEWMON = { {255,255,255}, {180,180,180}, {90,90,90}, {0,0,0} }
|
||||
}
|
||||
}
|
||||
Game.input = Input; Input:init()
|
||||
Game.stack = StateStack; StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
Game.save.player.name = "ASH"
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local done = false
|
||||
local diploma = Diploma.new(Game, function() done = true end)
|
||||
Game.stack:push(diploma)
|
||||
|
||||
check(diploma.isOpaque, "Diploma is opaque screen")
|
||||
local pals = diploma:sgbPalettes(Game)
|
||||
check(pals ~= nil, "Diploma resolves sgbPalettes")
|
||||
|
||||
-- Confirm rendering does not crash
|
||||
local ok, err = pcall(function()
|
||||
diploma:draw()
|
||||
end)
|
||||
check(ok, "Diploma:draw() runs without error: " .. tostring(err))
|
||||
|
||||
-- Confirm dismissal on A or B press
|
||||
Input.pressed = { a = true }
|
||||
diploma:update()
|
||||
check(done, "Diploma calls onDone on A press")
|
||||
eq(Game.stack:top(), nil, "Diploma pops from stack")
|
||||
|
||||
S.finish()
|
||||
@@ -61,11 +61,16 @@ local function fakeEngine(over)
|
||||
self.isLinked, self.codes = false, nil
|
||||
return true
|
||||
end,
|
||||
shareMods = function(self)
|
||||
self.calls[#self.calls + 1] = { "shareMods" }
|
||||
shareMods = function(self, withOptions)
|
||||
self.calls[#self.calls + 1] = { "shareMods", withOptions }
|
||||
self.shareCode = "K7QW3M"
|
||||
return true
|
||||
end,
|
||||
answerModOptions = function(self, importThem)
|
||||
self.calls[#self.calls + 1] = { "answerModOptions", importThem }
|
||||
if self.modPlan then self.modPlan.applyOptions = importThem and true or false end
|
||||
return importThem
|
||||
end,
|
||||
fetchShare = function(self, code)
|
||||
self.calls[#self.calls + 1] = { "fetchShare", code }
|
||||
return true
|
||||
@@ -148,7 +153,16 @@ eq(imp._syncModal, nil, "escape closes the modal")
|
||||
|
||||
imp:_openSync()
|
||||
imp:_syncView("mods")
|
||||
eq(imp._syncModal.withOptions, true,
|
||||
"sharing carries the options that go with the mods by default")
|
||||
imp:_syncShareMods()
|
||||
eq(eng.calls[#eng.calls][2], true, "so the engine is told to include them")
|
||||
imp:_syncToggleShareOptions()
|
||||
eq(imp._syncModal.withOptions, false, "the toggle turns them off")
|
||||
imp:_syncShareMods()
|
||||
eq(eng.calls[#eng.calls][2], false,
|
||||
"and a list can be shared with no options at all")
|
||||
imp:_syncToggleShareOptions()
|
||||
eq(eng.shareCode, "K7QW3M", "Share mod list asks the engine for a code")
|
||||
imp:_syncFocusField("share")
|
||||
imp:textinput("k7qw3m")
|
||||
@@ -216,6 +230,22 @@ check(labels["Share mod list"], "the mod view shares a list")
|
||||
check(labels["Get mod list"], "and fetches one")
|
||||
check(labels["Apply these mods"], "a fetched plan can be applied")
|
||||
|
||||
rEng.modPlan = { indexes = {}, toInstall = {}, toEnable = {}, missing = {},
|
||||
options = { { id = "biggermod", values = { speed = 1 } },
|
||||
{ id = "another-one", values = { theme = "dark" } } } }
|
||||
labels = controls(rImp)
|
||||
check(labels["Import their options"],
|
||||
"a list that carries options asks before importing them")
|
||||
check(labels["Keep my options"], "and offers to leave this device alone")
|
||||
check(not labels["Apply these mods"],
|
||||
"the question is answered before anything is applied")
|
||||
rImp:_syncAnswerModOptions(false)
|
||||
eq(rEng.calls[#rEng.calls][2], false, "the answer reaches the engine")
|
||||
labels = controls(rImp)
|
||||
check(labels["Apply these mods"], "and the apply road opens again")
|
||||
check(not labels["Import their options"], "with the question gone")
|
||||
rEng.modPlan = nil
|
||||
|
||||
rImp:_syncView("home")
|
||||
rEng.devices = {
|
||||
{ id = "0a1b2c3d", label = "OS X", current = true },
|
||||
@@ -304,4 +334,85 @@ check(pump and pump:find("self.launcher", 1, true) ~= nil,
|
||||
check(impSrc:find("_syncTypeInto", 1, true) ~= nil,
|
||||
"text input is routed through the code filter")
|
||||
|
||||
do
|
||||
local realCard = Kit.card
|
||||
local card
|
||||
Kit.card = function(x, y, w, h, variant)
|
||||
card = { x = x, y = y, w = w, h = h }
|
||||
realCard(x, y, w, h, variant)
|
||||
end
|
||||
|
||||
local sizes = {
|
||||
{ 1080, 2400 }, { 2400, 1080 }, { 1280, 720 }, { 720, 1280 },
|
||||
{ 640, 960 }, { 480, 800 }, { 960, 540 }, { 800, 480 },
|
||||
}
|
||||
local views = {
|
||||
{ "home", function() end },
|
||||
{ "devices", function(_, e)
|
||||
e.codes = { code1 = "1234-5678", code2 = "8765-4321" }
|
||||
e.devices = { { id = "0a1b2c3d", label = "OS X", current = true },
|
||||
{ id = "99998888", label = "Android" },
|
||||
{ id = "77776666", label = "Steam Deck" } }
|
||||
end },
|
||||
{ "link", function(i) i:_syncView("link") end },
|
||||
{ "mods", function(i, e)
|
||||
i:_syncView("mods")
|
||||
e.shareCode = "K7QW3M"
|
||||
e.modPlan = { indexes = { "https://x" }, toInstall = { { id = "a" } },
|
||||
toEnable = {}, missing = { { id = "z" } }, options = {} }
|
||||
end },
|
||||
{ "mod options", function(i, e)
|
||||
i:_syncView("mods")
|
||||
e.modPlan = { indexes = {}, toInstall = {}, toEnable = {}, missing = {},
|
||||
options = { { id = "biggermod" }, { id = "another-one" },
|
||||
{ id = "a-third-one" } } }
|
||||
end },
|
||||
{ "busy", function(i, e)
|
||||
i:_syncView("mods")
|
||||
e.isBusy = true
|
||||
e.status = "Uploading the mod list and options..."
|
||||
end },
|
||||
{ "conflict", function(_, e)
|
||||
e.phase = "conflict"
|
||||
e.conflicts = { { key = "red/abc", version = "red", overlap = true,
|
||||
localMeta = { savedAt = 1700000000, sessionStart = 1699999000,
|
||||
summary = { name = "ASH", badges = 3, timeText = "5:42", dexCount = 40 } },
|
||||
remoteMeta = { savedAt = 1700000500, sessionStart = 1699999500,
|
||||
summary = { name = "ASH", badges = 4, timeText = "6:10", dexCount = 44 } } } }
|
||||
end },
|
||||
}
|
||||
|
||||
local worst = { over = 0 }
|
||||
for _, view in ipairs(views) do
|
||||
for _, size in ipairs(sizes) do
|
||||
love.graphics.getDimensions = function() return size[1], size[2] end
|
||||
love.graphics.getPixelDimensions = love.graphics.getDimensions
|
||||
local e = fakeEngine({ isLinked = true })
|
||||
local i = launcher(e)
|
||||
i:_openSync()
|
||||
view[2](i, e)
|
||||
Kit.audit = {}
|
||||
card = nil
|
||||
local ok = pcall(LauncherView.draw, i)
|
||||
local rows = Kit.audit or {}
|
||||
Kit.audit = nil
|
||||
check(ok, ("the %s panel draws at %dx%d"):format(view[1], size[1], size[2]))
|
||||
for _, r in ipairs(rows) do
|
||||
if r.class == "control" and card then
|
||||
local over = math.max((r.x + r.w) - (card.x + card.w),
|
||||
(r.y + r.h) - (card.y + card.h))
|
||||
if over > worst.over then
|
||||
worst = { over = over, view = view[1], w = size[1], h = size[2],
|
||||
label = r.label }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
check(worst.over <= 0, ("no sync button leaves its card%s"):format(
|
||||
worst.over > 0 and (": %s %dx%d overflows by %d at '%s'"):format(
|
||||
worst.view, worst.w, worst.h, worst.over, worst.label) or ""))
|
||||
Kit.card = realCard
|
||||
end
|
||||
|
||||
T.finish("launcher_sync_modal")
|
||||
|
||||
@@ -553,4 +553,109 @@ do
|
||||
"and the failure reaches the status line")
|
||||
end
|
||||
|
||||
do
|
||||
local shared = {}
|
||||
local eng, transport = engine({
|
||||
["POST /sync/modshare"] = function(req)
|
||||
shared[#shared + 1] = Json.decode(req.body)
|
||||
return { code = 200, body = '{"code":"K7QW3M"}' }
|
||||
end,
|
||||
}, {})
|
||||
eng.modDeps = {
|
||||
installed = function()
|
||||
return { { id = "alpha", version = "1.0.0",
|
||||
enabledByVersion = { red = true } } }
|
||||
end,
|
||||
indexes = function() return {} end,
|
||||
modOptions = function() return { alpha = { speed = 3 } } end,
|
||||
setOptions = function() return true end,
|
||||
}
|
||||
|
||||
eng:shareMods(false)
|
||||
pump(eng)
|
||||
T.eq(shared[1].manifest.mods[1].options, nil,
|
||||
"a shared list can leave the player's options at home")
|
||||
T.eq(eng.shareCode, "K7QW3M", "and still mints a code")
|
||||
|
||||
eng:shareMods(true)
|
||||
pump(eng)
|
||||
T.eq(shared[2].manifest.mods[1].options.speed, 3,
|
||||
"or carry the options that go with those mods")
|
||||
T.eq(#transport.sent, 2, "one request each")
|
||||
end
|
||||
|
||||
do
|
||||
local eng = engine({
|
||||
["GET /sync/modshare"] = { code = 200, body =
|
||||
'{"code":"K7QW3M","manifest":{"rev":2,"indexes":[],"hasOptions":true,' ..
|
||||
'"mods":[{"id":"alpha","version":"1.0.0","enabledFor":["red"],' ..
|
||||
'"options":{"speed":1}}]}}' },
|
||||
}, {})
|
||||
local written = {}
|
||||
eng.modDeps = {
|
||||
installed = function()
|
||||
return { { id = "alpha", version = "1.0.0",
|
||||
enabledByVersion = { red = true } } }
|
||||
end,
|
||||
indexes = function() return {} end,
|
||||
findEntry = function() return nil end,
|
||||
addIndex = function() return true end,
|
||||
install = function() return true end,
|
||||
setEnabled = function() return true end,
|
||||
modOptions = function() return { alpha = { speed = 3 } } end,
|
||||
setOptions = function(id, values) written[id] = values return true end,
|
||||
}
|
||||
|
||||
eng:fetchShare("K7QW3M")
|
||||
pump(eng)
|
||||
T.eq(#eng.modPlan.options, 1, "a fetched list reports the options it carries")
|
||||
T.eq(eng.modPlan.applyOptions, nil, "without deciding for the player")
|
||||
T.check(eng.status:find("options", 1, true) ~= nil,
|
||||
"and the status line says there is a question to answer")
|
||||
local ask = eng:modOptionsAsk()
|
||||
T.eq(ask and ask[1], "alpha", "the launcher can name the mods it would touch")
|
||||
|
||||
eng:answerModOptions(false)
|
||||
T.eq(eng:modOptionsAsk(), nil, "answering closes the question")
|
||||
eng:applyModPlan()
|
||||
pump(eng)
|
||||
T.eq(next(written), nil, "declining leaves this device's options alone")
|
||||
|
||||
eng:fetchShare("K7QW3M")
|
||||
pump(eng)
|
||||
eng:answerModOptions(true)
|
||||
eng:applyModPlan()
|
||||
pump(eng)
|
||||
T.eq(written.alpha.speed, 1, "accepting writes the sharer's values")
|
||||
end
|
||||
|
||||
do
|
||||
local eng = engine({
|
||||
["GET /sync/modshare"] = { code = 200, body =
|
||||
'{"code":"K7QW3M","manifest":{"rev":2,"indexes":[],"hasOptions":true,' ..
|
||||
'"mods":[{"id":"alpha","version":"1.0.0","enabledFor":["red"],' ..
|
||||
'"options":{"speed":1}}]}}' },
|
||||
}, {})
|
||||
local written = {}
|
||||
eng.modDeps = {
|
||||
installed = function()
|
||||
return { { id = "alpha", version = "1.0.0",
|
||||
enabledByVersion = { red = true } } }
|
||||
end,
|
||||
indexes = function() return {} end,
|
||||
findEntry = function() return nil end,
|
||||
addIndex = function() return true end,
|
||||
install = function() return true end,
|
||||
setEnabled = function() return true end,
|
||||
modOptions = function() return { alpha = { speed = 3 } } end,
|
||||
setOptions = function(id, values) written[id] = values return true end,
|
||||
}
|
||||
eng:fetchShare("K7QW3M")
|
||||
pump(eng)
|
||||
eng:applyModPlan()
|
||||
pump(eng)
|
||||
T.eq(next(written), nil,
|
||||
"an apply that never asked imports nothing: silence is not consent")
|
||||
end
|
||||
|
||||
T.finish("sync_engine")
|
||||
|
||||
@@ -10,9 +10,14 @@ local function row(id, version, enabled, github)
|
||||
enabledByVersion = enabled }
|
||||
end
|
||||
|
||||
local function deps(installed, indexes, catalog)
|
||||
local calls = { installed = {}, enabled = {}, indexes = {} }
|
||||
local function deps(installed, indexes, catalog, modOptions)
|
||||
local calls = { installed = {}, enabled = {}, indexes = {}, options = {} }
|
||||
return calls, {
|
||||
modOptions = function() return modOptions or {} end,
|
||||
setOptions = function(id, values)
|
||||
calls.options[id] = values
|
||||
return true
|
||||
end,
|
||||
installed = function() return installed end,
|
||||
indexes = function() return indexes or {} end,
|
||||
addIndex = function(url)
|
||||
@@ -145,4 +150,123 @@ do
|
||||
T.eq(#calls.installed, 0, "without the rest of the plan having run yet")
|
||||
end
|
||||
|
||||
do
|
||||
local live = {
|
||||
alpha = { speed = 3, name = "ASH", on = true, bad = {} },
|
||||
zeta = {},
|
||||
}
|
||||
local _, d = deps({
|
||||
row("alpha", "2.1.0", { red = true }),
|
||||
row("zeta", "1.0.0", { red = true }),
|
||||
}, {}, {}, live)
|
||||
|
||||
local plain = SyncMods.build(d)
|
||||
T.eq(plain.mods[1].options, nil,
|
||||
"a mod list shares no options unless the player asks for it")
|
||||
T.eq(plain.hasOptions, nil, "and is not flagged as carrying any")
|
||||
|
||||
local full = SyncMods.build(d, true)
|
||||
T.eq(full.hasOptions, true, "opting in flags the list as carrying options")
|
||||
T.eq(full.mods[1].options.speed, 3, "the player's own values ride along")
|
||||
T.eq(full.mods[1].options.name, "ASH", "text values too")
|
||||
T.eq(full.mods[1].options.on, true, "and toggles")
|
||||
T.eq(full.mods[1].options.bad, nil,
|
||||
"a nested table is never sent: only scalars cross the wire")
|
||||
T.eq(full.mods[2].options, nil, "a mod with nothing set sends no bucket")
|
||||
end
|
||||
|
||||
do
|
||||
local wide = {}
|
||||
for i = 1, SyncMods.MAX_OPTION_KEYS + 20 do wide["k" .. i] = i end
|
||||
wide.huge = string.rep("x", SyncMods.MAX_OPTION_TEXT + 100)
|
||||
local _, d = deps({ row("alpha", "1.0.0", { red = true }) }, {}, {},
|
||||
{ alpha = wide })
|
||||
local manifest = SyncMods.build(d, true)
|
||||
local n = 0
|
||||
for _ in pairs(manifest.mods[1].options) do n = n + 1 end
|
||||
T.eq(n, SyncMods.MAX_OPTION_KEYS, "an option bucket is capped")
|
||||
local kept = manifest.mods[1].options.huge
|
||||
T.check(kept == nil or #kept == SyncMods.MAX_OPTION_TEXT,
|
||||
"and a long string is clamped when it makes the cut")
|
||||
end
|
||||
|
||||
do
|
||||
local manifest = { rev = 2, indexes = {}, hasOptions = true, mods = {
|
||||
{ id = "alpha", version = "2.1.0", enabledFor = { "red" },
|
||||
options = { speed = 1, name = "MISTY" } },
|
||||
{ id = "beta", version = "1.0.0", enabledFor = { "red" },
|
||||
options = { theme = "dark" } },
|
||||
{ id = "same", version = "1.0.0", enabledFor = { "red" },
|
||||
options = { pitch = 5 } },
|
||||
{ id = "ghost", version = "0.1.0", enabledFor = { "red" },
|
||||
options = { anything = 1 } },
|
||||
} }
|
||||
local calls, d = deps(
|
||||
{ row("alpha", "2.1.0", { red = true }), row("same", "1.0.0", { red = true }) },
|
||||
{}, { beta = { id = "beta" } },
|
||||
{ alpha = { speed = 3, name = "ASH" }, same = { pitch = 5 } })
|
||||
|
||||
local plan = SyncMods.plan(manifest, d)
|
||||
T.eq(#plan.options, 2, "only mods this device can actually run are listed")
|
||||
T.eq(plan.options[1].id, "alpha", "the installed one whose values differ")
|
||||
T.eq(plan.options[2].id, "beta", "and the one this plan installs")
|
||||
for _, row in ipairs(plan.options) do
|
||||
T.check(row.id ~= "same", "a mod already set that way is not busywork")
|
||||
T.check(row.id ~= "ghost", "and a mod that cannot be installed is skipped")
|
||||
end
|
||||
T.eq(plan.applyOptions, nil, "nobody's options are imported unasked")
|
||||
T.eq(SyncMods.planHasOptions(plan), true, "the plan reports it has some")
|
||||
T.eq(SyncMods.optionsAnswered(plan), false, "and that the question is open")
|
||||
|
||||
local steps = SyncMods.steps(plan, d)
|
||||
local labels = 0
|
||||
for _, step in ipairs(steps) do
|
||||
if step.label == "alpha" then labels = labels + 1 end
|
||||
end
|
||||
T.eq(labels, 0, "an unanswered plan writes no options")
|
||||
|
||||
SyncMods.answerOptions(plan, false)
|
||||
T.eq(SyncMods.optionsAnswered(plan), true, "declining answers the question")
|
||||
SyncMods.apply(plan, nil, d)
|
||||
T.eq(next(calls.options), nil, "and keeps the options this device already had")
|
||||
|
||||
SyncMods.answerOptions(plan, true)
|
||||
T.eq(plan.applyOptions, true, "accepting arms the option steps")
|
||||
SyncMods.apply(plan, nil, d)
|
||||
T.eq(calls.options.alpha.speed, 1, "the sharer's values are written")
|
||||
T.eq(calls.options.alpha.name, "MISTY", "every key they set")
|
||||
T.eq(calls.options.beta.theme, "dark", "including a mod installed by the plan")
|
||||
end
|
||||
|
||||
do
|
||||
local manifest = { rev = 2, indexes = {}, mods = {
|
||||
{ id = "alpha", version = "1.0.0", enabledFor = { "red" },
|
||||
options = { speed = 1 } } } }
|
||||
local calls, d = deps({ row("alpha", "1.0.0", { red = true }) }, {}, {},
|
||||
{ alpha = { speed = 3 } })
|
||||
local plan = SyncMods.plan(manifest, d)
|
||||
T.eq(SyncMods.planEmpty(plan), true,
|
||||
"a list that only differs in options plans no mod work")
|
||||
SyncMods.answerOptions(plan, true)
|
||||
T.eq(SyncMods.planEmpty(plan), false,
|
||||
"until the options are accepted, and then there is work to do")
|
||||
d.install = function() return nil, "download failed" end
|
||||
SyncMods.apply(plan, nil, d)
|
||||
T.eq(calls.options.alpha.speed, 1, "which is just the option write")
|
||||
end
|
||||
|
||||
do
|
||||
local manifest = { rev = 2, indexes = {}, mods = {
|
||||
{ id = "beta", version = "1.0.0", enabledFor = { "red" },
|
||||
options = { speed = 1 } } } }
|
||||
local calls, d = deps({}, {}, { beta = { id = "beta" } })
|
||||
d.install = function() return nil, "download failed" end
|
||||
local plan = SyncMods.plan(manifest, d)
|
||||
SyncMods.answerOptions(plan, true)
|
||||
local ok = SyncMods.apply(plan, nil, d)
|
||||
T.eq(ok, false, "a failed install still fails the apply")
|
||||
T.eq(next(calls.options), nil,
|
||||
"and the options of a mod that never installed are not written")
|
||||
end
|
||||
|
||||
T.finish("sync_mods")
|
||||
|
||||
@@ -236,6 +236,32 @@ do
|
||||
same(z[2].colors[3], BLUE_LOGO1[3], "Blue LOGO1's other inks stay put")
|
||||
end
|
||||
|
||||
-- #133's grays overlay follows every box on screen, not just the topmost
|
||||
-- state's: DisplayContinueGameInfo leaves the menu box up behind the info
|
||||
-- window (main_menu.asm:36-39), and reading only the top left the menu box
|
||||
-- on the raw LOGO2 / LOGO1 bands -- blue rows over a red EXIT GAME row.
|
||||
do
|
||||
PaletteFX.mode = "gbc"
|
||||
GameVersion.set("red")
|
||||
local stack = {
|
||||
states = { { isOpaque = true },
|
||||
{ titleUiBox = { 0, 0, 12, 9 } },
|
||||
{ titleUiBox = { 4, 7, 19, 16 } } },
|
||||
visibleBase = function() return 1 end,
|
||||
top = function(self) return self.states[#self.states] end,
|
||||
}
|
||||
local z = title:sgbPalettes({ data = romPack(RED_LOGO1), stack = stack })
|
||||
eq(#z, 5, "both open boxes get an overlay zone")
|
||||
eq(z[4].x, 0, "the menu box keeps its overlay while the info window is up")
|
||||
eq(z[4].y, 0, "at the menu box's own origin")
|
||||
eq(z[5].x, 32, "and the info window's sits on top of it")
|
||||
eq(z[5].y, 56, "at hlcoord 4,7")
|
||||
|
||||
stack.states[3] = nil
|
||||
z = title:sgbPalettes({ data = romPack(RED_LOGO1), stack = stack })
|
||||
eq(#z, 4, "the menu alone is still one overlay, as before")
|
||||
end
|
||||
|
||||
PaletteFX.mode = savedMode
|
||||
GameVersion.set(savedVersion)
|
||||
|
||||
|
||||
@@ -67,6 +67,37 @@ T.eq(anchorsAfter({ centered = false, hold = true }), 0,
|
||||
"a battle still holds the anchors while DYNAMIC is on")
|
||||
T.eq(anchorsAfter({ centered = true, hold = true }), 0, "and with it off")
|
||||
|
||||
-- --------------------------------------------------------- the save panel
|
||||
|
||||
-- PrintSaveScreenText prints at hlcoord 4,0 over the kept-open START menu box
|
||||
-- at 9,0 (start_sub_menus.asm:641-647); docking the menu alone split it (#1619).
|
||||
do
|
||||
local StartMenu = require("src.ui.StartMenu")
|
||||
local DataFx = T.fixtures.load()
|
||||
require("src.render.Font").load(DataFx)
|
||||
local pushed = {}
|
||||
local panelGame = {
|
||||
data = DataFx, save = SaveData.newGame(),
|
||||
stack = { states = pushed,
|
||||
push = function(_, s) pushed[#pushed + 1] = s end,
|
||||
pop = function() end,
|
||||
top = function() return pushed[#pushed] end },
|
||||
}
|
||||
panelGame.save.player.name = "RED"
|
||||
local menu = StartMenu.new(panelGame)
|
||||
pushed[#pushed + 1] = menu
|
||||
local saveRow
|
||||
for _, item in ipairs(menu.items) do
|
||||
if tostring(item.label):match("SAVE") then saveRow = item end
|
||||
end
|
||||
T.check(saveRow ~= nil, "the START menu carries a SAVE row")
|
||||
T.eq(Game.uiAnchorsHeldInStack({ states = pushed }), false,
|
||||
"the START menu alone still docks")
|
||||
saveRow.onSelect()
|
||||
T.eq(Game.uiAnchorsHeldInStack({ states = pushed }), true,
|
||||
"the SAVE panel holds the anchors so it cannot be split from the menu")
|
||||
end
|
||||
|
||||
-- ------------------------------------------------- the scale half of it
|
||||
|
||||
-- CENTERED is a FIXED letterbox, so the UI must not follow the survey zoom
|
||||
|
||||
@@ -785,6 +785,14 @@ REQUIRED_SYMBOLS = {
|
||||
"KabutoPuzzleLZ", "OmanytePuzzleLZ", "AerodactylPuzzleLZ", "HoOhPuzzleLZ",
|
||||
"UnownPuzzleStartCancelLZ", "UnownPuzzleCursorGFX",
|
||||
"PuzzlePieceBorderData.TileBordersGFX",
|
||||
# Goldenrod Game Corner (engine/games/slot_machine.asm + card_flip.asm).
|
||||
# Slots1LZ/2LZ/3LZ are the reel + actor sheets; SlotsTilemap is the 20x12
|
||||
# BG map. CardFlipLZ01..03 + On/Off button tiles and CardFlipTilemap are
|
||||
# the odds-board art. Without these in the manifest the extractor skips
|
||||
# the files and SlotMachine/CardFlip fall back to labelled cells.
|
||||
"Slots1LZ", "Slots2LZ", "Slots3LZ", "SlotsTilemap",
|
||||
"CardFlipLZ01", "CardFlipLZ02", "CardFlipLZ03",
|
||||
"CardFlipOnButtonGFX", "CardFlipOffButtonGFX", "CardFlipTilemap",
|
||||
# Emote bubbles (data/sprites/emotes.asm): showemote's ! over a trainer
|
||||
# who just spotted the player, and the other faces scripts use.
|
||||
"ShockEmote", "QuestionEmote", "HappyEmote", "SadEmote",
|
||||
|
||||
@@ -17667,6 +17667,46 @@
|
||||
"wBaseUnusedFrontpic": [
|
||||
1,
|
||||
53554
|
||||
],
|
||||
"Slots1LZ": [
|
||||
36,
|
||||
31138
|
||||
],
|
||||
"Slots2LZ": [
|
||||
36,
|
||||
31522
|
||||
],
|
||||
"Slots3LZ": [
|
||||
36,
|
||||
32130
|
||||
],
|
||||
"SlotsTilemap": [
|
||||
36,
|
||||
30898
|
||||
],
|
||||
"CardFlipLZ01": [
|
||||
56,
|
||||
21795
|
||||
],
|
||||
"CardFlipLZ02": [
|
||||
56,
|
||||
22197
|
||||
],
|
||||
"CardFlipLZ03": [
|
||||
56,
|
||||
21736
|
||||
],
|
||||
"CardFlipOnButtonGFX": [
|
||||
56,
|
||||
21779
|
||||
],
|
||||
"CardFlipOffButtonGFX": [
|
||||
56,
|
||||
21763
|
||||
],
|
||||
"CardFlipTilemap": [
|
||||
56,
|
||||
22809
|
||||
]
|
||||
},
|
||||
"tilesets": {
|
||||
|
||||
@@ -17667,6 +17667,46 @@
|
||||
"wBaseUnusedFrontpic": [
|
||||
1,
|
||||
53554
|
||||
],
|
||||
"Slots1LZ": [
|
||||
36,
|
||||
31138
|
||||
],
|
||||
"Slots2LZ": [
|
||||
36,
|
||||
31522
|
||||
],
|
||||
"Slots3LZ": [
|
||||
36,
|
||||
32130
|
||||
],
|
||||
"SlotsTilemap": [
|
||||
36,
|
||||
30898
|
||||
],
|
||||
"CardFlipLZ01": [
|
||||
56,
|
||||
21795
|
||||
],
|
||||
"CardFlipLZ02": [
|
||||
56,
|
||||
22197
|
||||
],
|
||||
"CardFlipLZ03": [
|
||||
56,
|
||||
21736
|
||||
],
|
||||
"CardFlipOnButtonGFX": [
|
||||
56,
|
||||
21779
|
||||
],
|
||||
"CardFlipOffButtonGFX": [
|
||||
56,
|
||||
21763
|
||||
],
|
||||
"CardFlipTilemap": [
|
||||
56,
|
||||
22809
|
||||
]
|
||||
},
|
||||
"tilesets": {
|
||||
|
||||
Reference in New Issue
Block a user