mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-23 14:07:34 +02:00
@@ -39,6 +39,21 @@ local function messageCopy(screen)
|
||||
return #lines > 0 and lines or nil
|
||||
end
|
||||
|
||||
local function itemCopies(game, screen, catchable)
|
||||
local out = {}
|
||||
for id, count in pairs((game.save and game.save.inventory) or {}) do
|
||||
local def = game.data.items and game.data.items[id]
|
||||
if count > 0 and def and def.pocket == "BALL" then
|
||||
out[#out + 1] = { id = id, name = def.name or id, count = count,
|
||||
ball = true, needsTarget = false,
|
||||
catchChance = catchable and screen.catchChance
|
||||
and screen:catchChance(id) or nil }
|
||||
end
|
||||
end
|
||||
table.sort(out, function(a, b) return a.name < b.name end)
|
||||
return out
|
||||
end
|
||||
|
||||
local function signature(game, screen, top)
|
||||
if not screen then return "none" end
|
||||
local battle = screen.battle or {}
|
||||
@@ -56,6 +71,9 @@ local function signature(game, screen, top)
|
||||
parts[#parts + 1] = tostring(mon.hp)
|
||||
parts[#parts + 1] = tostring(mon.status)
|
||||
end
|
||||
for _, item in ipairs(itemCopies(game, screen, false)) do
|
||||
parts[#parts + 1] = item.id .. "=" .. tostring(item.count)
|
||||
end
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
@@ -111,9 +129,9 @@ function BattleAPI:snapshot()
|
||||
player = monCopy(game.data, battle.player, true),
|
||||
enemy = monCopy(game.data, battle.enemy, true),
|
||||
party = party, moves = moveCopies(game, battle),
|
||||
-- Gold's PACK is pocketed and target selection is screen-owned. Omit it
|
||||
-- until the engine can expose the same semantic item records as Gen 1.
|
||||
items = {} }
|
||||
-- Targeted medicine remains screen-owned, but balls are complete semantic
|
||||
-- records and can safely expose the same read-only preview as Gen 1.
|
||||
items = itemCopies(game, screen, battle.wild and not screen.tutorial) }
|
||||
end
|
||||
|
||||
local MENU_CHOICES = { fight = true, party = true, item = true, run = true }
|
||||
|
||||
@@ -305,6 +305,15 @@ function Catching.rate(opts)
|
||||
return math.min(255, rate), false
|
||||
end
|
||||
|
||||
-- Exact stock catch probability for read-only previews. A catch.rate hook
|
||||
-- may replace the roll entirely, so nil is safer than presenting a guess.
|
||||
function Catching.chance(opts)
|
||||
if Runtime.wantsHook("catch.rate") then return nil end
|
||||
local rate, guaranteed = Catching.rate(opts)
|
||||
if guaranteed or rate >= 255 then return 100 end
|
||||
return rate * 100 / 256
|
||||
end
|
||||
|
||||
-- The status half of the rate, off the merged `statuses` record the same way
|
||||
-- src/battle/Catching.lua reads record.catchBonus on Gen 1. Gold's records
|
||||
-- live on src/battle/gen2/Battle.lua (Battle.STATUSES) and carry BOTH numbers:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
local Platform = require("src.core.Platform")
|
||||
local HostShell = require("src.core.HostShell")
|
||||
|
||||
local FilePicker = {}
|
||||
|
||||
FilePicker.IMAGE = {
|
||||
label = "Image",
|
||||
exts = { "png", "jpg", "jpeg" },
|
||||
tempName = "pokeport_image_pick",
|
||||
}
|
||||
|
||||
local function trim(value)
|
||||
return value and value:gsub("^%s+", ""):gsub("%s+$", "") or ""
|
||||
end
|
||||
|
||||
local function shellSafe(s)
|
||||
s = tostring(s):gsub("%%", "%%%%")
|
||||
return s:gsub('"', '\\"'):gsub("'", "''")
|
||||
end
|
||||
|
||||
local function commandOutput(command)
|
||||
if not Platform.canSpawnProcess() then return nil end
|
||||
local pipe = HostShell.popen(command)
|
||||
if not pipe then return nil end
|
||||
local result = pipe:read("*a")
|
||||
HostShell.pclose(pipe)
|
||||
result = trim(result)
|
||||
return result ~= "" and result or nil
|
||||
end
|
||||
|
||||
local function appleTypes(exts)
|
||||
local out = {}
|
||||
for _, ext in ipairs(exts) do out[#out + 1] = '"' .. ext .. '"' end
|
||||
return table.concat(out, ", ")
|
||||
end
|
||||
|
||||
local function windowsPatterns(exts)
|
||||
local out = {}
|
||||
for _, ext in ipairs(exts) do out[#out + 1] = "*." .. ext end
|
||||
return table.concat(out, ";")
|
||||
end
|
||||
|
||||
local function globPatterns(exts)
|
||||
local out = {}
|
||||
for _, ext in ipairs(exts) do out[#out + 1] = "*." .. ext end
|
||||
return table.concat(out, " ")
|
||||
end
|
||||
|
||||
function FilePicker.available()
|
||||
return Platform.canSpawnProcess()
|
||||
end
|
||||
|
||||
function FilePicker.matches(name, kind)
|
||||
local lower = tostring(name or ""):lower()
|
||||
for _, ext in ipairs(kind.exts) do
|
||||
if lower:match("%." .. ext .. "$") then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function FilePicker.open(prompt, kind)
|
||||
if not Platform.canSpawnProcess() then return nil end
|
||||
local title = shellSafe(prompt)
|
||||
local platform = love.system.getOS()
|
||||
if platform == "OS X" then
|
||||
return commandOutput(
|
||||
([[osascript -e 'POSIX path of (choose file with prompt "%s" of type {%s})' 2>/dev/null]])
|
||||
:format(title, appleTypes(kind.exts)))
|
||||
elseif platform == "Windows" then
|
||||
local script = table.concat({
|
||||
"Add-Type -AssemblyName System.Windows.Forms;",
|
||||
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
|
||||
"$d.Title='" .. title .. "';",
|
||||
"$d.Filter='" .. kind.label .. " (" .. windowsPatterns(kind.exts) .. ")|"
|
||||
.. windowsPatterns(kind.exts) .. "|All files (*.*)|*.*';",
|
||||
"if($d.ShowDialog() -eq 'OK'){",
|
||||
"$n=[IO.Path]::GetFileName($d.FileName) -replace '[^\\x20-\\x7E]','_';",
|
||||
"$t=Join-Path $env:TEMP $n;",
|
||||
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
|
||||
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
||||
"[Console]::Write($t)}",
|
||||
})
|
||||
return commandOutput(
|
||||
'powershell -NoProfile -STA -Command "' .. script .. '"')
|
||||
elseif platform == "Linux" then
|
||||
local path = commandOutput(
|
||||
([[zenity --file-selection --title="%s" --file-filter="%s | %s" 2>/dev/null]])
|
||||
:format(title, kind.label, globPatterns(kind.exts)))
|
||||
if path then return path end
|
||||
return commandOutput(([[kdialog --getopenfilename "$HOME" "%s|%s" 2>/dev/null]])
|
||||
:format(globPatterns(kind.exts), kind.label))
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function FilePicker.read(path)
|
||||
local file, openError = io.open(path, "rb")
|
||||
if not file then return nil, openError end
|
||||
local data = file:read("*a")
|
||||
file:close()
|
||||
if not data or data == "" then return nil, "empty file" end
|
||||
return data
|
||||
end
|
||||
|
||||
function FilePicker.basename(path)
|
||||
return tostring(path or ""):match("([^/\\]+)$") or tostring(path or "")
|
||||
end
|
||||
|
||||
return FilePicker
|
||||
+73
-33
@@ -2017,6 +2017,23 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
local gap = m.gap
|
||||
local cy = y
|
||||
|
||||
local title = Strings("Skins/Borders")
|
||||
local bh = m.btnH
|
||||
local importLabel = imp:_skinsImportButtonLabel()
|
||||
local importW = Kit.textWidth("small", importLabel) + math.floor(24 * m.s)
|
||||
if Kit.textWidth("button", title) + importW + math.floor(24 * m.s) > w then
|
||||
importLabel = Strings("Import")
|
||||
importW = Kit.textWidth("small", importLabel) + math.floor(20 * m.s)
|
||||
end
|
||||
local place = Layout.rightCluster(x, w, math.floor(6 * m.s))
|
||||
btn(imp, place(importW), cy, importW, bh, "skins-import", importLabel, {
|
||||
kind = "accent", font = "small",
|
||||
action = function() imp:chooseSkin() end })
|
||||
Kit.text("button", Kit.ellipsize("button", title,
|
||||
math.max(0, w - importW - math.floor(12 * m.s))), x,
|
||||
cy + math.floor((bh - Kit.textHeight("button")) / 2), PAL.heading)
|
||||
cy = cy + bh + math.floor(8 * m.s)
|
||||
|
||||
if imp._skinNotice then
|
||||
cy = cy + Kit.textWrapped("small", imp._skinNotice.text, x, cy, w,
|
||||
imp._skinNotice.ok and PAL.green or PAL.red, 2) + math.floor(8 * m.s)
|
||||
@@ -2075,30 +2092,57 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
cy = cy + rowH + math.floor(4 * m.s)
|
||||
end
|
||||
|
||||
skinRow("skin-none", nil, Strings("Built-in pad"),
|
||||
Strings("The default on-screen buttons."), active == nil,
|
||||
imp.onEditTouchControls and function()
|
||||
imp.onEditTouchControls(imp.modScope or "red")
|
||||
end or nil)
|
||||
local entries = { false }
|
||||
for _, entry in ipairs(skins) do entries[#entries + 1] = entry end
|
||||
|
||||
for _, entry in ipairs(skins) do
|
||||
local bits = {}
|
||||
bits[#bits + 1] = entry.source == "user" and Strings("installed")
|
||||
or Strings("bundled")
|
||||
if entry.controls > 0 then
|
||||
bits[#bits + 1] = entry.controls .. " " .. Strings("buttons")
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
local hint = Strings(
|
||||
"You can also drop a skin .zip on this window, or put a folder in %s/ of your save directory. RetroArch overlay .cfg files work as-is.",
|
||||
TouchSkin.USER_ROOT)
|
||||
local hintH = Kit.wrapHeight("small", hint, w, 3)
|
||||
local importH = math.floor(10 * m.s) + hintH
|
||||
|
||||
local rowGap = math.floor(4 * m.s)
|
||||
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||
local listTop = cy
|
||||
local listH = availH - (cy - y) - importH
|
||||
local perPage = Kit.rowsThatFit(listH, rowH, rowGap, 1, 20)
|
||||
if #entries > perPage then
|
||||
perPage = Kit.rowsThatFit(listH - pagerH - gap, rowH, rowGap, 1, 20)
|
||||
end
|
||||
local first, last, cur, pages = Kit.pageBounds(page(imp, "skins"),
|
||||
#entries, perPage)
|
||||
setPage(imp, "skins", cur)
|
||||
setPage(imp, "skins",
|
||||
Kit.wheelPage(x, listTop, w, listH, cur, #entries, perPage))
|
||||
|
||||
for i = first, last do
|
||||
local entry = entries[i]
|
||||
if not entry then
|
||||
skinRow("skin-none", nil, Strings("Built-in pad"),
|
||||
Strings("The default on-screen buttons."), active == nil,
|
||||
imp.onEditTouchControls and function()
|
||||
imp.onEditTouchControls(imp.modScope or "red")
|
||||
end or nil)
|
||||
else
|
||||
bits[#bits + 1] = Strings("bezel only")
|
||||
local bits = {}
|
||||
bits[#bits + 1] = entry.source == "user" and Strings("installed")
|
||||
or Strings("bundled")
|
||||
if entry.controls > 0 then
|
||||
bits[#bits + 1] = entry.controls .. " " .. Strings("buttons")
|
||||
else
|
||||
bits[#bits + 1] = Strings("bezel only")
|
||||
end
|
||||
if entry.pages > 1 then
|
||||
bits[#bits + 1] = entry.pages .. " " .. Strings("pages")
|
||||
end
|
||||
if entry.screen then bits[#bits + 1] = Strings("screen cutout") end
|
||||
local configure = imp.onOpenSkinStudio and function()
|
||||
imp.onOpenSkinStudio(imp.modScope or "red", entry.id)
|
||||
end or nil
|
||||
skinRow("skin-" .. entry.id, entry.id, entry.id,
|
||||
table.concat(bits, " \194\183 "), active == entry.id, configure)
|
||||
end
|
||||
if entry.pages > 1 then
|
||||
bits[#bits + 1] = entry.pages .. " " .. Strings("pages")
|
||||
end
|
||||
if entry.screen then bits[#bits + 1] = Strings("screen cutout") end
|
||||
local configure = imp.onOpenSkinStudio and function()
|
||||
imp.onOpenSkinStudio(imp.modScope or "red", entry.id)
|
||||
end or nil
|
||||
skinRow("skin-" .. entry.id, entry.id, entry.id,
|
||||
table.concat(bits, " \194\183 "), active == entry.id, configure)
|
||||
end
|
||||
|
||||
if #skins == 0 then
|
||||
@@ -2107,18 +2151,14 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
cy = cy + math.floor(72 * m.s) + gap
|
||||
end
|
||||
|
||||
cy = cy + math.floor(6 * m.s)
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
Kit.caption(x, cy, Strings("IMPORT"))
|
||||
cy = cy + Kit.textHeight("small") + math.floor(6 * m.s)
|
||||
local boxH = math.floor(76 * m.s)
|
||||
Kit.card(x, cy, w, boxH, "muted")
|
||||
Kit.textWrapped("small", Strings(
|
||||
"Drop a skin .zip on this window to install it, or put a folder in the skins folder of your save directory. RetroArch overlay .cfg files work as-is."),
|
||||
x + math.floor(14 * m.s), cy + math.floor(12 * m.s),
|
||||
w - math.floor(28 * m.s), PAL.muted, 3)
|
||||
Kit.text("small", TouchSkin.USER_ROOT .. "/", x + math.floor(14 * m.s),
|
||||
cy + boxH - Kit.textHeight("small") - math.floor(10 * m.s), PAL.faint)
|
||||
if pages > 1 then
|
||||
setPage(imp, "skins",
|
||||
Kit.pager(x, cy, w, cur, #entries, perPage, "skins"))
|
||||
cy = cy + pagerH + gap
|
||||
end
|
||||
|
||||
cy = cy + math.floor(10 * m.s)
|
||||
Kit.textWrapped("small", hint, x, cy, w, PAL.muted, 3)
|
||||
end
|
||||
|
||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
|
||||
+148
-10
@@ -988,8 +988,18 @@ local function pickerHasKind(kind)
|
||||
return false
|
||||
end
|
||||
|
||||
local function findPendingRequiredImport()
|
||||
local function findPendingRequiredImport(self)
|
||||
local names = { "picked_required_import.bin", "picked_stadium.z64" }
|
||||
-- Builds released before required_import was added to the Android JNI bridge
|
||||
-- only understand the long-standing "rom" picker kind. While a required
|
||||
-- import request is in flight, it is safe to treat its staging name as a
|
||||
-- dependency file: the pending IDs below select the same validation/copy
|
||||
-- path as a current bridge. Never scan picked_rom.gb otherwise, since that
|
||||
-- remains reserved for an ordinary game-ROM import.
|
||||
if self and self.requiredImportLegacyRomPick
|
||||
and self.pickerPendingKind == "required_import" then
|
||||
names[#names + 1] = "picked_rom.gb"
|
||||
end
|
||||
for _, name in ipairs(names) do
|
||||
if love.filesystem.getInfo(name, "file") then return name end
|
||||
end
|
||||
@@ -1089,6 +1099,39 @@ local function chooseZip()
|
||||
return nil
|
||||
end
|
||||
|
||||
local function chooseSkinZip()
|
||||
local prompt = shellSafe(Strings("Choose a skin .zip"))
|
||||
local platform = love.system.getOS()
|
||||
if platform == "OS X" then
|
||||
return commandOutput(
|
||||
([[osascript -e 'POSIX path of (choose file with prompt "%s" of type {"zip"})' 2>/dev/null]])
|
||||
:format(prompt))
|
||||
elseif platform == "Windows" then
|
||||
local script = table.concat({
|
||||
"Add-Type -AssemblyName System.Windows.Forms;",
|
||||
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
|
||||
"$d.Title='" .. prompt .. "';",
|
||||
"$d.Filter='Skin archive (*.zip)|*.zip|All files (*.*)|*.*';",
|
||||
"if($d.ShowDialog() -eq 'OK'){",
|
||||
"$n=[IO.Path]::GetFileName($d.FileName) -replace '[^\\x20-\\x7E]','_';",
|
||||
"$t=Join-Path $env:TEMP $n;",
|
||||
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
|
||||
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
||||
"[Console]::Write($t)}",
|
||||
})
|
||||
return commandOutput(
|
||||
'powershell -NoProfile -STA -Command "' .. script .. '"')
|
||||
elseif platform == "Linux" then
|
||||
local path = commandOutput(
|
||||
([[zenity --file-selection --title="%s" --file-filter="Skin archive | *.zip" 2>/dev/null]])
|
||||
:format(prompt))
|
||||
if path then return path end
|
||||
return commandOutput(
|
||||
[[kdialog --getopenfilename "$HOME" "*.zip|Skin archive" 2>/dev/null]])
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Open a native picker for a raw .sav battery save (mirrors chooseZip's per-OS
|
||||
-- dialogs). Returns the chosen absolute path or nil. Android uses
|
||||
-- love.system.pickFile("sav") instead -- see RomImporter:chooseSaveImport.
|
||||
@@ -1471,14 +1514,23 @@ function RomImporter:focus(f)
|
||||
local text = "Could not read the picked file. Reopen the picker and choose "
|
||||
.. "it with the Files (Documents) app, or copy it into: "
|
||||
.. love.filesystem.getSaveDirectory()
|
||||
local legacyRequiredPick = self.requiredImportLegacyRomPick
|
||||
and self.pickerPendingKind == "required_import"
|
||||
if pickError:find("picked_required_import", 1, true)
|
||||
or pickError:find("picked_stadium", 1, true) then
|
||||
or pickError:find("picked_stadium", 1, true)
|
||||
or (legacyRequiredPick and pickError:find("picked_rom", 1, true)) then
|
||||
self.modNotice = { ok = false, text = text }
|
||||
self.pickerPendingKind = nil
|
||||
self.pickerPendingModId = nil
|
||||
self.pickerPendingImportId = nil
|
||||
self.requiredImportLegacyRomPick = nil
|
||||
elseif pickError:find("picked_mod", 1, true) then
|
||||
self.modNotice = { ok = false, text = text }
|
||||
if self.pickerPendingKind == "skin" then
|
||||
self.pickerPendingKind = nil
|
||||
self._skinNotice = { ok = false, text = text }
|
||||
else
|
||||
self.modNotice = { ok = false, text = text }
|
||||
end
|
||||
elseif pickError:find("picked_save", 1, true) then
|
||||
local version = self.androidPendingVersion or self:_savedropTarget()
|
||||
self.androidPendingVersion = nil
|
||||
@@ -1488,11 +1540,12 @@ function RomImporter:focus(f)
|
||||
end
|
||||
return
|
||||
end
|
||||
local requiredName = findPendingRequiredImport()
|
||||
local requiredName = findPendingRequiredImport(self)
|
||||
if requiredName then
|
||||
local modId, importId = self.pickerPendingModId, self.pickerPendingImportId
|
||||
self.pickerPendingKind = nil
|
||||
self.pickerPendingModId, self.pickerPendingImportId = nil, nil
|
||||
self.requiredImportLegacyRomPick = nil
|
||||
local imported = modId and importId
|
||||
and self:_importRequiredSource(modId, importId, requiredName)
|
||||
consumePick(self, requiredName, requiredName, imported)
|
||||
@@ -1504,6 +1557,13 @@ function RomImporter:focus(f)
|
||||
end
|
||||
local modName = findPendingMod(false, self.pickSkip)
|
||||
if modName then
|
||||
if self.pickerPendingKind == "skin" then
|
||||
self.pickerPendingKind = nil
|
||||
self:_installSkinZip(modName)
|
||||
consumePick(self, modName, "picked_mod.zip",
|
||||
self._skinNotice and self._skinNotice.ok)
|
||||
return
|
||||
end
|
||||
self:_installMod(modName)
|
||||
consumePick(self, modName, "picked_mod.zip",
|
||||
self.modNotice and self.modNotice.ok)
|
||||
@@ -2024,7 +2084,17 @@ function RomImporter:chooseRequiredImport(modId, importId)
|
||||
return
|
||||
end
|
||||
if self.nativePicker then
|
||||
if self.mobileFileBridge and not pickerHasKind("required_import") then
|
||||
-- Android 13+ uses the Storage Access Framework for both paths. Some
|
||||
-- Android 15 installs carry the newer Lua launcher with an older native
|
||||
-- bridge, however, so they do not advertise required_import yet. Fall
|
||||
-- back to that bridge's known "rom" picker and quarantine its result by
|
||||
-- the pending required-import IDs. iOS has a different asynchronous
|
||||
-- bridge and deliberately keeps the explicit capability requirement.
|
||||
local legacyAndroidPicker = self.mobileFileBridge
|
||||
and love.system.getOS() == "Android"
|
||||
and not pickerHasKind("required_import")
|
||||
if self.mobileFileBridge and not pickerHasKind("required_import")
|
||||
and not legacyAndroidPicker then
|
||||
requiredImportNotice(self, modId, importId,
|
||||
"This app build cannot pick required mod files yet. Update the app and try again.")
|
||||
self.modNotice = nil
|
||||
@@ -2033,10 +2103,12 @@ function RomImporter:chooseRequiredImport(modId, importId)
|
||||
self.pickerPendingKind = "required_import"
|
||||
self.pickerPendingModId = modId
|
||||
self.pickerPendingImportId = importId
|
||||
if not pickFile("required_import") then
|
||||
self.requiredImportLegacyRomPick = legacyAndroidPicker or nil
|
||||
if not pickFile(legacyAndroidPicker and "rom" or "required_import") then
|
||||
self.pickerPendingKind = nil
|
||||
self.pickerPendingModId = nil
|
||||
self.pickerPendingImportId = nil
|
||||
self.requiredImportLegacyRomPick = nil
|
||||
requiredImportNotice(self, modId, importId, "Could not open the file picker.")
|
||||
self.modNotice = nil
|
||||
elseif self.android then
|
||||
@@ -2498,6 +2570,11 @@ function RomImporter:update(dt)
|
||||
if Platform.isUWP() and self.modNotice and self.modNotice.ok then
|
||||
os.remove(path)
|
||||
end
|
||||
elseif kind == "skin" then
|
||||
self:_installSkinZip(path)
|
||||
if Platform.isUWP() and self._skinNotice and self._skinNotice.ok then
|
||||
os.remove(path)
|
||||
end
|
||||
elseif kind == "sav" then
|
||||
local target = version or self:_savedropTarget()
|
||||
self:_importSave(target, path)
|
||||
@@ -2520,6 +2597,8 @@ function RomImporter:update(dt)
|
||||
self.pickerPendingModId, self.pickerPendingImportId = nil, nil
|
||||
elseif kind == "mod" then
|
||||
self.modNotice = { ok = false, text = errorText }
|
||||
elseif kind == "skin" then
|
||||
self._skinNotice = { ok = false, text = errorText }
|
||||
elseif kind == "sav" then
|
||||
self.saveNotice[version] = { ok = false, text = errorText }
|
||||
else
|
||||
@@ -3047,13 +3126,27 @@ function RomImporter:_useSkin(id)
|
||||
}
|
||||
end
|
||||
|
||||
function RomImporter:_installSkinZip(file)
|
||||
function RomImporter:_installSkinZip(source)
|
||||
if self.workState == "working" then return end
|
||||
self.tab = "skins"
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
local name = file:getFilename() or ""
|
||||
local data, readError = readDroppedFile(file)
|
||||
local name, data, readError
|
||||
if type(source) == "string" then
|
||||
name = source
|
||||
if not source:match("^/") and not source:match("^%a:[/\\]")
|
||||
and not source:match("^[Ss][Dd][Mm][Cc]:") then
|
||||
data = love.filesystem.read(source)
|
||||
end
|
||||
if not data then data, readError = readExternalPath(source) end
|
||||
if not data then data = love.filesystem.read(source) end
|
||||
else
|
||||
name = source:getFilename() or ""
|
||||
data, readError = readDroppedFile(source)
|
||||
end
|
||||
if not data then
|
||||
self._skinNotice = { ok = false,
|
||||
text = "Could not read the dropped file: " .. tostring(readError) }
|
||||
text = "Could not read the skin archive: "
|
||||
.. tostring(readError or name) }
|
||||
return
|
||||
end
|
||||
local id, err = TouchSkin.installArchive(name, data)
|
||||
@@ -3065,6 +3158,51 @@ function RomImporter:_installSkinZip(file)
|
||||
self._skinNotice = { ok = true, text = "Imported " .. id }
|
||||
end
|
||||
|
||||
function RomImporter:_skinsImportButtonLabel()
|
||||
if self.isNX then return Strings("Scan again") end
|
||||
return Strings("Import skin .zip")
|
||||
end
|
||||
|
||||
function RomImporter:chooseSkin()
|
||||
if self.workState == "working" then return end
|
||||
if self.isNX then
|
||||
local found = #self:_ensureSkins(true)
|
||||
self._skinNotice = { ok = true, text = Strings(
|
||||
"%d skins found. Copy a skin .zip into %s/ over MTP, then scan again.",
|
||||
found, require("src.core.TouchSkin").USER_ROOT) }
|
||||
return
|
||||
end
|
||||
if self.nativePicker and love.system.getPickedFile then
|
||||
self.pickerPendingKind = "skin"
|
||||
if not pickFile("mod") then
|
||||
self.pickerPendingKind = nil
|
||||
self._skinNotice = { ok = false, text = "Could not open the file picker." }
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.android then
|
||||
local name = findPendingMod(true, self.pickSkip)
|
||||
if name then
|
||||
self:_installSkinZip(name)
|
||||
consumePick(self, name, "picked_mod.zip",
|
||||
self._skinNotice and self._skinNotice.ok)
|
||||
return
|
||||
end
|
||||
self.pickerPendingKind = "skin"
|
||||
if not pickFile("mod") then
|
||||
self.pickerPendingKind = nil
|
||||
self._skinNotice = { ok = false,
|
||||
text = "Could not open the file picker. Copy a skin .zip via USB." }
|
||||
else
|
||||
self.pickPending = true
|
||||
self.pickTimer = 0
|
||||
end
|
||||
return
|
||||
end
|
||||
local path = chooseSkinZip()
|
||||
if path then self:_installSkinZip(path) end
|
||||
end
|
||||
|
||||
function RomImporter:_toggleFindSearchFocus()
|
||||
self._findSearchFocus = not self._findSearchFocus
|
||||
if self._findSearchFocus then
|
||||
|
||||
+98
-18
@@ -4,6 +4,7 @@ local PAL = Theme.PAL
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
|
||||
local Studio = {}
|
||||
|
||||
@@ -214,12 +215,19 @@ function Studio.cycleImage(dir)
|
||||
markDirty()
|
||||
end
|
||||
|
||||
function Studio.imageTargetLabel()
|
||||
local target = Studio.imageTarget
|
||||
if target == "bezel" or not Studio.selectedControl() then return "bezel" end
|
||||
return target == "pressed" and "pressed art" or "idle art"
|
||||
end
|
||||
|
||||
function Studio.assignImage(rel)
|
||||
local page, ctl = Studio.page(), Studio.selectedControl()
|
||||
local img = TouchSkin.resolveImage(Studio.skin.root, rel)
|
||||
if ctl and Studio.imageTarget == "pressed" then
|
||||
local target = Studio.imageTarget
|
||||
if ctl and target == "pressed" then
|
||||
ctl.pressedImagePath, ctl.pressedImage = rel, img
|
||||
elseif ctl and Studio.imageTarget == "idle" then
|
||||
elseif ctl and target == "idle" then
|
||||
ctl.imagePath, ctl.image = rel, img
|
||||
elseif page then
|
||||
page.imagePath, page.image = rel, img
|
||||
@@ -228,11 +236,69 @@ function Studio.assignImage(rel)
|
||||
Studio.dirty = true
|
||||
end
|
||||
|
||||
local function commitSkinId()
|
||||
local skin = Studio.skin
|
||||
if not skin then return end
|
||||
local id = (Studio.skinIdField or ""):gsub("[^%w_%-]", "")
|
||||
if id == "" or id == skin.id then return end
|
||||
TouchSkin.saveTo(skin, id)
|
||||
Studio.available = TouchSkin.list()
|
||||
end
|
||||
|
||||
function Studio.adoptImage(name, data, target)
|
||||
if not Studio.skin then return false end
|
||||
if target then Studio.imageTarget = target end
|
||||
if not FilePicker.matches(name, FilePicker.IMAGE) then
|
||||
Studio.status = "Pick a PNG or JPG."
|
||||
return false
|
||||
end
|
||||
commitSkinId()
|
||||
local rel, err = TouchSkin.importImage(Studio.skin, name, data)
|
||||
if not rel then
|
||||
Studio.status = "Import failed: " .. tostring(err)
|
||||
return false
|
||||
end
|
||||
local where = Studio.imageTargetLabel()
|
||||
Studio.assignImage(rel)
|
||||
Studio.skinIdField = Studio.skin.id
|
||||
Studio.status = "Imported " .. rel .. " as " .. where
|
||||
if where == "bezel" and not Studio.canvas().lockViewport then
|
||||
Studio.status = Studio.status
|
||||
.. " -- use Detect screen from bezel to place the screen"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Studio.importImageFile(target)
|
||||
if not Studio.skin then return end
|
||||
target = target or Studio.imageTarget
|
||||
Studio.imageTarget = target
|
||||
if target ~= "bezel" and not Studio.selectedControl() then
|
||||
Studio.status = "Select a control first, or import a bezel image."
|
||||
return
|
||||
end
|
||||
if not FilePicker.available() then
|
||||
Studio.status = "No file picker here -- drag a PNG onto the window instead."
|
||||
return
|
||||
end
|
||||
local prompt = (target == "bezel") and "Choose a bezel image"
|
||||
or "Choose a button image"
|
||||
local path = FilePicker.open(prompt, FilePicker.IMAGE)
|
||||
if not path then return end
|
||||
local base = FilePicker.basename(path)
|
||||
local data, err = FilePicker.read(path)
|
||||
if not data then
|
||||
Studio.status = "Could not read " .. base .. ": " .. tostring(err)
|
||||
return
|
||||
end
|
||||
Studio.adoptImage(base, data, target)
|
||||
end
|
||||
|
||||
function Studio.filedropped(file)
|
||||
if not Studio.skin then return end
|
||||
local path = (file.getFilename and file:getFilename()) or ""
|
||||
local base = path:match("([^/\\]+)$") or path
|
||||
if not base:lower():match("%.png$") and not base:lower():match("%.jpe?g$") then
|
||||
local base = FilePicker.basename(path)
|
||||
if not FilePicker.matches(base, FilePicker.IMAGE) then
|
||||
Studio.status = "Drop a PNG or JPG to use it as art."
|
||||
return
|
||||
end
|
||||
@@ -246,17 +312,7 @@ function Studio.filedropped(file)
|
||||
Studio.status = "Could not read " .. base
|
||||
return
|
||||
end
|
||||
local rel, err = TouchSkin.importImage(Studio.skin, base, data)
|
||||
if not rel then
|
||||
Studio.status = "Import failed: " .. tostring(err)
|
||||
return
|
||||
end
|
||||
Studio.assignImage(rel)
|
||||
local where = Studio.selectedControl()
|
||||
and (Studio.imageTarget == "pressed" and "pressed art" or "idle art")
|
||||
or "bezel"
|
||||
Studio.status = "Imported " .. rel .. " as " .. where
|
||||
Studio.skinIdField = Studio.skin.id
|
||||
Studio.adoptImage(base, data)
|
||||
end
|
||||
|
||||
function Studio.detectViewport()
|
||||
@@ -617,10 +673,16 @@ local function inspectorBody(x, y, w)
|
||||
|
||||
if page then
|
||||
local bezel = page.imagePath or "(none)"
|
||||
if Kit.button(x, cy, w, rowH, "Bezel: " .. bezel, { id = "bezel" }) then
|
||||
local pickW = 82 * Kit.scale
|
||||
local cycleW = w - pickW - gap
|
||||
if Kit.button(x, cy, cycleW, rowH, "Bezel: " .. bezel, { id = "bezel" }) then
|
||||
Studio.imageTarget = "bezel"
|
||||
Studio.cycleImage(1)
|
||||
end
|
||||
if Kit.button(x + cycleW + gap, cy, pickW, rowH, "Import",
|
||||
{ id = "bezelpick" }) then
|
||||
Studio.importImageFile("bezel")
|
||||
end
|
||||
cy = cy + rowH + gap
|
||||
local vpLabel = page.viewport and "Screen cutout: ON" or "Screen cutout: OFF"
|
||||
if Kit.button(x, cy, half, rowH, vpLabel, { id = "vp",
|
||||
@@ -701,17 +763,25 @@ local function inspectorBody(x, y, w)
|
||||
Kit.text("small", ("canvas %dx%d px"):format(canvas.w, canvas.h), x, cy, PAL.faint)
|
||||
cy = cy + Kit.textHeight("small") + gap
|
||||
|
||||
local pickW = 82 * Kit.scale
|
||||
local artW = w - pickW - gap
|
||||
local idle = ctl.imagePath or "(none)"
|
||||
if Kit.button(x, cy, w, rowH, "Idle art: " .. idle, { id = "img" }) then
|
||||
if Kit.button(x, cy, artW, rowH, "Idle art: " .. idle, { id = "img" }) then
|
||||
Studio.imageTarget = "idle"
|
||||
Studio.cycleImage(1)
|
||||
end
|
||||
if Kit.button(x + artW + gap, cy, pickW, rowH, "Import", { id = "imgpick" }) then
|
||||
Studio.importImageFile("idle")
|
||||
end
|
||||
cy = cy + rowH + gap
|
||||
local pressed = ctl.pressedImagePath or "(none)"
|
||||
if Kit.button(x, cy, w, rowH, "Pressed art: " .. pressed, { id = "imgp" }) then
|
||||
if Kit.button(x, cy, artW, rowH, "Pressed art: " .. pressed, { id = "imgp" }) then
|
||||
Studio.imageTarget = "pressed"
|
||||
Studio.cycleImage(1)
|
||||
end
|
||||
if Kit.button(x + artW + gap, cy, pickW, rowH, "Import", { id = "imgppick" }) then
|
||||
Studio.importImageFile("pressed")
|
||||
end
|
||||
return cy + rowH
|
||||
end
|
||||
|
||||
@@ -885,6 +955,16 @@ function Studio.wheelmoved(_, dy)
|
||||
Studio.wheel = dy
|
||||
end
|
||||
|
||||
function Studio.focus()
|
||||
Studio.drag = nil
|
||||
Studio.clicked = false
|
||||
if Studio.testing then TouchControls:reset() end
|
||||
end
|
||||
|
||||
function Studio.visible()
|
||||
Studio.focus()
|
||||
end
|
||||
|
||||
function Studio.textinput(text)
|
||||
Kit.textinput(text)
|
||||
end
|
||||
|
||||
+33
-30
@@ -2966,6 +2966,37 @@ function BattleState:openDexEntry(species)
|
||||
})
|
||||
end
|
||||
|
||||
function BattleState:catchOptions(itemId)
|
||||
local data = self.game and self.game.data or {}
|
||||
local battle = self.battle
|
||||
local enemy = battle and battle.enemy
|
||||
if not enemy then return nil end
|
||||
local enemyDef = data.pokemon and data.pokemon[enemy.species]
|
||||
local dexEntry = data.gen2Pokedex and data.gen2Pokedex[enemy.species]
|
||||
local evolveItem
|
||||
for _, entry in ipairs((enemyDef and enemyDef.evolutions) or {}) do
|
||||
if entry.method == "EVOLVE_ITEM" then evolveItem = entry.item end
|
||||
end
|
||||
local player = battle.player
|
||||
return {
|
||||
battle = battle, mon = enemy, def = enemyDef,
|
||||
maxHp = enemy.maxHp or (enemy.stats and enemy.stats.hp), hp = enemy.hp,
|
||||
catchRate = enemyDef and enemyDef.catchRate or 45, ball = itemId,
|
||||
status = enemy.status, random = battle.random,
|
||||
weight = dexEntry and dexEntry.weight, level = enemy.level,
|
||||
playerLevel = player and player.level,
|
||||
fishing = battle.battleType == "fish", species = enemy.species,
|
||||
gender = enemy.gender, playerSpecies = player and player.species,
|
||||
playerGender = player and player.gender, evolveItem = evolveItem,
|
||||
}
|
||||
end
|
||||
|
||||
function BattleState:catchChance(itemId)
|
||||
if self.tutorial then return 100 end
|
||||
local opts = self:catchOptions(itemId)
|
||||
return opts and Catching.chance(opts) or nil
|
||||
end
|
||||
|
||||
-- Items in battle: balls try a catch, the stat items apply their stage, and
|
||||
-- everything with a ported party effect runs the same item_effects.asm routine
|
||||
-- the field pack runs. Anything else reports that it cannot be used, which is
|
||||
@@ -2995,7 +3026,6 @@ function BattleState:useItem(itemId)
|
||||
return
|
||||
end
|
||||
local enemy = self.battle.enemy
|
||||
local enemyDef = data.pokemon and data.pokemon[enemy.species]
|
||||
local caught, rate
|
||||
if self.tutorial then
|
||||
-- `ld a, [wBattleType] / cp BATTLETYPE_TUTORIAL /
|
||||
@@ -3009,35 +3039,8 @@ function BattleState:useItem(itemId)
|
||||
caught, rate = true, 255
|
||||
else
|
||||
-- The specialty-ball conditions (BallMultiplierFunctionTable): each one
|
||||
-- is something this screen already knows. Heavy Ball reads the dex
|
||||
-- weight, Moon Ball the species' stone row, Love Ball both genders,
|
||||
-- Level Ball the two levels, Lure Ball wBattleType.
|
||||
local dexEntry = data.gen2Pokedex and data.gen2Pokedex[enemy.species]
|
||||
local evolveItem
|
||||
for _, entry in ipairs((enemyDef and enemyDef.evolutions) or {}) do
|
||||
if entry.method == "EVOLVE_ITEM" then evolveItem = entry.item end
|
||||
end
|
||||
local player = self.battle.player
|
||||
caught, rate = Catching.attempt({
|
||||
battle = self.battle,
|
||||
mon = enemy,
|
||||
def = enemyDef,
|
||||
maxHp = enemy.maxHp or (enemy.stats and enemy.stats.hp),
|
||||
hp = enemy.hp,
|
||||
catchRate = enemyDef and enemyDef.catchRate or 45,
|
||||
ball = itemId,
|
||||
status = enemy.status,
|
||||
random = self.battle.random,
|
||||
weight = dexEntry and dexEntry.weight,
|
||||
level = enemy.level,
|
||||
playerLevel = player and player.level,
|
||||
fishing = self.battle.battleType == "fish",
|
||||
species = enemy.species,
|
||||
gender = enemy.gender,
|
||||
playerSpecies = player and player.species,
|
||||
playerGender = player and player.gender,
|
||||
evolveItem = evolveItem,
|
||||
})
|
||||
-- is also used by the read-only preview, so both paths stay exact.
|
||||
caught, rate = Catching.attempt(self:catchOptions(itemId))
|
||||
end
|
||||
-- wWildMon carries the answer through the animation, and
|
||||
-- wThrownBallWobbleCount is the counter GetPokeBallWobble bumps once per
|
||||
|
||||
Reference in New Issue
Block a user