Files
gen1recomp/tools/save-editor/InspectorBody.lua
T
2026-10-02 06:21:06 -04:00

501 lines
17 KiB
Lua

local Ops = require("Ops")
local Gen = require("Gen")
local P = require("Properties")
local MonOps = require("MonOps")
local PAL = require("Theme").PAL
local Body = {}
local Motion = require("Motion")
local Chooser = require("Chooser")
local Touch = require("TouchEditor")
local Limits = require("ValueLimits")
local Named = require("NamedChoices")
local function drawSection(S, Kit, x, y, w, h, report, issues)
local mon = S.editingMon
if not mon then
Kit.textWrapped(
"button",
"Select a Pokemon to edit its properties, stats, moves and origin.",
x + 16,
y + 24,
w - 32,
PAL.muted
)
return
end
local s, pad, gap = Kit.scale, 14 * Kit.scale, 10 * Kit.scale
local row = Kit.controlH()
local cx, inner = x + pad, w - 2 * pad
local g = Gen.ofState(S)
local sections = {
{ "main", "Main" },
{ "stats", "Stats" },
{ "moves", "Moves" },
{ "origin", "Origin" },
{ "extras", "Extras" },
{ "checks", "Checks" },
}
for _, entry in ipairs(sections) do
entry.errors = issues.sections[entry[1]]
entry.icon = entry.errors > 0 and "triangle-alert" or Kit.navigationIcon(entry[2])
end
S.monSection = S.monSection or "main"
local navY = y + pad
local navX = cx
if S.inspectorBack then
if Kit.iconButton(cx, navY, row, row, "chevron-left", "Back to party") then
Motion.change(S, "mobileInspector", false, -1)
end
navX = cx + row + gap
end
Chooser.navigation(
S,
Kit,
"monSection",
"Pokemon section",
sections,
navX,
navY,
math.min(cx + inner - navX, 360 * s),
row
)
local bodyY = navY + row + gap
local bodyH = math.max(0, y + h - pad - bodyY)
if S.formMon ~= mon then
S.formMon, S.monDrafts, S.inspectorScroll = mon, {}, 0
S._formHeight = 0
S.propertyChoice = nil
if Kit.focus and Kit.focus:match("^property%-") then
Kit.blur()
end
end
S.monDrafts = S.monDrafts or {}
S.inspectorScroll =
Kit.scrollPixels(cx, bodyY, inner, bodyH, S.inspectorScroll or 0, S._formHeight or 0)
Kit.pushClip(cx, bodyY, inner, bodyH)
local cy = bodyY - S.inspectorScroll
local start = cy
local function text(str, color)
cy = cy + Kit.textWrapped("small", str, cx, cy, inner, color or PAL.caption) + gap
end
local function hint(id)
cy = cy + Touch.issue(Kit, issues.fields[id], cx, cy, inner)
end
if report.errors > 0 then
text(report.errors .. " saved value errors. Check the red fields.", PAL.red)
end
local function button(label, fn, kind, help, id)
local issue = id and issues.fields[id]
local opts = { kind = kind, font = "small", invalid = issue ~= nil }
local buttonH = Kit.buttonHeight(label, inner, opts)
if help then
buttonH = Touch.action(S, Kit, label, help, fn, cx, cy, inner, kind, issue)
elseif Kit.button(cx, cy, inner, buttonH, label, opts) then
fn()
end
cy = cy + buttonH + gap
if issue and not help then hint(id) end
end
local function field(id, label, value, fn, sanitize)
if type(value) == "number" then
label = label:gsub(" %(.-%)", "")
cy = cy
+ Touch.value(S, Kit, id, label, value, function()
return Limits.mon(S, mon, id)
end, cx, cy, inner, fn, issues.fields[id])
+ 2 * gap
return
end
text(label, issues.fields[id] and PAL.red or PAL.text)
local key = "property-" .. id
local draft = S.monDrafts[id]
if Kit.focus ~= key and draft == nil then
draft = tostring(value or "")
end
local function apply(v)
if type(value) == "number" then
local n = tonumber(v)
if not n or n ~= math.floor(n) then
return Ops.say(S, "Enter a whole number")
end
end
if fn(v) then
S.monDrafts[id] = nil
end
end
local applyW = math.max(row, Kit.textWidth("small", "Set") + 24 * s)
S.monDrafts[id] = Kit.textfield(
key,
cx,
cy,
inner - applyW - gap,
row,
draft or tostring(value or ""),
"value",
{ sanitize = sanitize, onSubmit = apply, invalid = issues.fields[id] ~= nil }
)
if
Kit.button(cx + inner - applyW, cy, applyW, row, "Set", { kind = "accent", font = "small" })
then
apply(S.monDrafts[id])
Kit.blur()
end
cy = cy + row + 2 * gap
hint(id)
end
local function choice(id, label, value, options, apply)
cy = cy + Touch.choice(S, Kit, id, label, value, options, cx, cy, inner, apply, nil, issues.fields[id]) + 2 * gap
end
local function props(list)
for _, d in ipairs(list) do
if d.toggle then
local on = P.get(mon, d)
on = on == true or on == 1
local issue = issues.fields[d.key]
local shown = issue and tostring(P.get(mon, d)) or (on and "ON" or "OFF")
local label = d.label .. ": " .. shown
local opts = { face = "selection", active = on, font = "small", invalid = issue ~= nil }
local height = Kit.buttonHeight(label, inner, opts)
if Kit.button(cx, cy, inner, height, label, opts) then
Ops.setMonProperty(S, mon, d.key, not on)
end
cy = cy + height + gap
hint(d.key)
elseif Named.property(S, d) then
choice(d.key, d.label, P.get(mon, d), Named.property(S, d), function(v)
return Ops.setMonProperty(S, mon, d.key, v)
end)
else
field(d.key, d.label, P.get(mon, d), function(v)
return Ops.setMonProperty(S, mon, d.key, v)
end)
end
end
end
local def = (S.data and S.data.pokemon and S.data.pokemon[mon.species or mon.speciesId]) or {}
if S.monSection == "main" then
text(def.name or tostring(mon.species or mon.speciesId), PAL.heading)
button("Change species", function()
Ops.openSpeciesPicker(S, Kit)
end, "accent", nil, "species")
if S.nicknameMon ~= mon then
S.nicknameMon, S.nicknameDraft = mon, mon.nickname or ""
end
text("Nickname (up to 10 game characters)", PAL.text)
local setW = math.max(row, 64 * s)
S.nicknameDraft = Kit.textfield(
"mon-nickname",
cx,
cy,
inner - setW - gap,
row,
S.nicknameDraft or "",
"no nickname",
{
invalid = issues.fields.nickname ~= nil,
sanitize = function(v)
return Ops.nicknameSanitize(S, v)
end,
}
)
if Kit.button(cx + inner - setW, cy, setW, row, "Set", { kind = "accent", font = "small" }) then
Ops.setNickname(S, mon, S.nicknameDraft)
Kit.blur()
end
cy = cy + row + gap
hint("nickname")
button("Clear nickname", function()
Ops.clearNickname(S, mon)
S.nicknameDraft = ""
end, "danger")
field("level", "Level (1-100)", mon.level, function(v)
local n = tonumber(v)
if not require("Legality").integer(n, 1, 100) then
return Ops.say(S, "Level must be a whole number from 1 to 100")
end
return Ops.setLevel(S, mon, n)
end)
field("experience", "Experience", Gen.exp(mon), function(v)
return Ops.setExperience(S, mon, v)
end)
field(
"current-hp",
"Current HP (max " .. tostring(mon.maxHp or (mon.stats and mon.stats.hp) or 0) .. ")",
mon.hp or 0,
function(v)
return Ops.setCurrentHp(S, mon, v)
end
)
local statuses = {
{ "healthy", "Healthy" },
{ "SLP", "Asleep" },
{ "PSN", "Poisoned" },
{ "BRN", "Burned" },
{ "FRZ", "Frozen" },
{ "PAR", "Paralyzed" },
}
if g >= 2 then
statuses[#statuses + 1] = { "TOX", "Badly poisoned" }
end
local savedStatus = mon.status or "healthy"
if type(savedStatus) == "number" then
savedStatus = ({ [0]="healthy",[8]="PSN",[16]="BRN",[32]="FRZ",[64]="PAR",[128]="TOX" })[savedStatus]
or (savedStatus >= 1 and savedStatus <= 7 and "SLP") or savedStatus
end
choice("status", "Status", savedStatus, statuses, function(v)
return Ops.setMonStatus(S, mon, v ~= "healthy" and v or nil)
end)
if g >= 2 then
local held = S.data.items and S.data.items[mon.heldItem or mon.item]
button("Held item: " .. tostring(held and held.name or "None"), function()
Ops.openItemPicker(S, Kit, "held")
end, "accent", nil, "heldItem")
button("Clear held item", function()
Ops.setHeldItem(S, mon, nil)
end, "danger")
field("friendship", "Friendship (0-255)", mon.friendship or mon.happiness or 0, function(v)
local n = tonumber(v)
if not require("Legality").integer(n, 0, 255) then
return Ops.say(S, "Friendship must be 0-255")
end
return Ops.setHappiness(S, mon, n)
end)
end
if g == 3 then
local Pokemon = require("src.core.game3.pokemon")
local Summary = require("src.core.game3.summary_data")
local nature = mon.nature or (tonumber(mon.personality) or 0) % 25
local natures = {}
for id = 0, 24 do
local ok, name = pcall(function()
return Summary.NATURES[id]
end)
natures[#natures + 1] = { id, ok and name or tostring(id) }
end
choice("nature", "Nature", nature, natures, function(v)
return Ops.setNature(S, mon, v)
end)
local slot = mon.abilityNum or (tonumber(mon.personality) or 0) % 2
local abilities = {}
for i, id in ipairs(Pokemon.abilities(mon.species)) do
if id and id ~= 0 then
abilities[#abilities + 1] = { i - 1, Pokemon.abilityName(id) }
end
end
choice("ability", "Ability", slot, abilities, function(v)
return Ops.setAbility(S, mon, v)
end)
local gender = mon.gender or Pokemon.gender(mon.species, tonumber(mon.personality) or 0)
local ratio = (Pokemon.speciesMeta(mon.species) or {}).genderRatio or 255
local genders = ratio == 255 and { { "U", "Genderless" } }
or ratio == 0 and { { "M", "Male" } }
or ratio == 254 and { { "F", "Female" } }
or { { "M", "Male" }, { "F", "Female" } }
choice("gender", "Gender", gender, genders, function(v)
return Ops.setMonGender(S, mon, v)
end)
local shiny = mon.isShiny
if shiny == nil then shiny = Pokemon.isShiny(mon) end
button("Shiny: " .. (shiny and "ON" or "OFF"), function()
Ops.setShiny(S, mon, not shiny)
end, "warn", "Changes shininess and personality. Review Checks after editing.", "shiny")
elseif g == 2 then
local d = P.find(S, "pokerus")
choice("pokerus", "Pokérus", mon.pokerus or 0, Named.property(S, d), function(v)
return Ops.setPokerus(S, mon, v)
end)
text("Gender, shininess and Unown form follow DVs.")
for _, id in ipairs({ "gender", "shiny", "form" }) do hint(id) end
end
button("Fix all errors", function()
Ops.fixMonErrors(S, mon)
end, "good", "Fixes invalid values, stats and PP. Origin warnings still need checking.")
button(
"Randomize Pokémon",
function()
Ops.randomizeMon(S, mon)
end,
"accent",
"Replaces this Pokémon with a wild one from this game. Real level range, normal moves. Undo brings yours back."
)
button(
"Max out Pokémon",
function()
Ops.maxMon(S, mon)
end,
"good",
"Level 100, max IVs or DVs, friendship and PP. Spare EVs go to the strongest stats. Fully heals."
)
button("Full heal", function()
Ops.healMon(S, mon)
end, "good")
button("Clone to a box", function()
Ops.cloneMonToBox(S, mon)
end, "accent")
elseif S.monSection == "stats" then
local keys = g == 3 and { "hp", "atk", "def", "spa", "spd", "spe" }
or { "hp", "attack", "defense", "speed", "special" }
local stats = mon.stats or {}
text(
("Calculated: HP %s / Atk %s / Def %s / Spe %s"):format(
tostring(stats.hp or mon.maxHp or "?"),
tostring(stats.attack or mon.attack or "?"),
tostring(stats.defense or mon.defense or "?"),
tostring(stats.speed or mon.speed or "?")
),
issues.fields.calculated and PAL.red or PAL.heading
)
text(
g == 1 and ("Special: " .. tostring(stats.special or "?"))
or (
"Sp. Atk: "
.. tostring(stats.specialAttack or stats.spAtk or mon.spAtk or "?")
.. " / Sp. Def: "
.. tostring(stats.specialDefense or stats.spDef or mon.spDef or "?")
),
issues.fields.calculated and PAL.red or PAL.heading
)
hint("calculated")
text(
g == 3 and "IVs 0-31. EVs 0-255 with a total limit of 510."
or "DVs 0-15. HP DV follows the other DVs. Stat experience 0-65535."
)
if g == 3 then
button("Max all IVs", function()
Ops.maxIvs(S, mon)
end, "good", "Sets all six IVs to 31. Origin checks may still need review.")
button("Clear EVs", function()
Ops.clearEvs(S, mon)
end, "danger")
local total = 0
for _, k in ipairs(keys) do
total = total + (mon.evs and mon.evs[k] or 0)
end
text(
"EVs: " .. total .. " / 510 · " .. math.max(0, 510 - total) .. " free",
total > 510 and PAL.red or PAL.green
)
else
button("Max all DVs", function()
Ops.maxDvs(S, mon)
end, "good", "Sets DVs to 15. In Gen 2 this can change gender and shininess.")
button("Max stat training", function()
Ops.maxStatExp(S, mon)
end, "good", "Fills stat experience for every stat.")
text("HP DV: " .. tostring(mon.dvs and mon.dvs.hp or 0) .. " / 15 · follows the other DVs", issues.fields["dv-hp"] and PAL.red)
hint("dv-hp")
end
for _, k in ipairs(keys) do
if g == 3 then
field(
"iv-" .. k,
(
{ hp = "HP", atk = "Attack", def = "Defense", spa = "Sp. Atk", spd = "Sp. Def", spe = "Speed" }
)[k] .. " IV",
mon.ivs and mon.ivs[k] or 0,
function(v)
return Ops.setIv(S, mon, k, tonumber(v) or 0)
end
)
field(
"ev-" .. k,
(
{ hp = "HP", atk = "Attack", def = "Defense", spa = "Sp. Atk", spd = "Sp. Def", spe = "Speed" }
)[k] .. " EV",
mon.evs and mon.evs[k] or 0,
function(v)
return Ops.setEv(S, mon, k, tonumber(v) or 0)
end
)
else
if k ~= "hp" then
field("dv-" .. k, k:upper() .. " DV", mon.dvs and mon.dvs[k] or 0, function(v)
return Ops.setDv(S, mon, k, tonumber(v) or 0)
end)
end
field(
"se-" .. k,
k:upper() .. " stat experience",
mon.statExp and mon.statExp[k] or 0,
function(v)
return Ops.setStatExp(S, mon, k, v)
end
)
end
end
elseif S.monSection == "moves" then
for slot = 1, 4 do
local mv = mon.moves and mon.moves[slot]
local id = type(mv) == "table" and (mv.moveId or mv.id) or mv
local md = id and S.data.moves and S.data.moves[id]
button("Slot " .. slot .. ": " .. tostring(md and md.name or id or "empty"), function()
Ops.openMovePicker(S, Kit, slot)
end, "accent", nil, "move" .. slot)
if id and id ~= 0 then
local pp = type(mv) == "table" and mv.pp or mon.pp and mon.pp[slot] or 0
local ups = MonOps.getPpUps(mon, slot)
local max = MonOps.calcMaxPp(MonOps.getBasePp(S.data, mon, slot), ups, g)
field("pp-" .. slot, "Current PP (max " .. max .. ")", pp, function(v)
return Ops.setPp(S, mon, slot, tonumber(v) or 0)
end)
field("ppup-" .. slot, "PP Ups (0-3)", ups, function(v)
return Ops.setPpUps(S, mon, slot, tonumber(v) or 0)
end)
button("Clear slot " .. slot, function()
Ops.clearMove(S, mon, slot)
end, "danger")
end
end
button("Reset to learnset", function()
Ops.resetMoves(S, mon)
end)
button("Max all PP", function()
Ops.maxAllPpUps(S, mon)
end, "good")
elseif S.monSection == "origin" then
props(P.identity(S))
if g == 2 and Gen.hasCaughtData(S.save, S.version) then
button("Caught by: " .. tostring(mon.caughtByGender or "none"), function()
Ops.setCaughtByGender(S, mon, mon.caughtByGender == "boy" and "girl" or "boy")
end)
end
elseif S.monSection == "extras" then
if g == 3 then
text("Contest conditions", issues.fields.contest and PAL.red)
hint("contest")
props(P.contest)
text("Ribbons: setting a ribbon does not establish award or event eligibility.", issues.fields.ribbons and PAL.red)
hint("ribbons")
props(P.ribbons)
else
text("This generation has no contest conditions or ribbons.")
end
else
button("Fix all errors", function()
Ops.fixMonErrors(S, mon)
end, "good", "Fixes invalid values, stats and PP. Does not invent encounter or event history.")
text(
report.errors > 0 and (report.errors .. " property errors")
or "Values pass. Origin still needs checking.",
report.errors > 0 and PAL.red or PAL.yellow
)
for _, check in ipairs(report.checks) do
text(
check.kind:upper() .. ": " .. check.message,
check.kind == "error" and PAL.red or check.kind == "pass" and PAL.green or PAL.yellow
)
end
end
S._formHeight = cy - start
Kit.popClip()
Kit.scrollbar(cx, bodyY, inner, bodyH, S.inspectorScroll, S._formHeight, bodyH)
end
function Body.draw(S, Kit, x, y, w, h)
local report = require("Legality").mon(S, S.editingMon)
local issues = require("Legality").highlights(report, S.editingMon)
Motion.pages(S, Kit, "monSection", x, y, w, h, function(state, kit, px, py, pw, ph)
drawSection(state, kit, px, py, pw, ph, report, issues)
end)
end
return Body