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

403 lines
13 KiB
Lua

local Copy = require("src.mods.Merge").deepCopy
local Gen = require("Gen")
local M = require("MonOps")
local P = require("Properties")
local Limits = require("ValueLimits")
local A = {}
local function clamp(v, lo, hi)
v = tonumber(v)
if not v or v ~= v or v == math.huge or v == -math.huge then
v = lo
end
return math.max(lo, math.min(hi, math.floor(v)))
end
local function replace(mon, staged)
for k in pairs(mon) do
mon[k] = nil
end
for k, v in pairs(staged) do
mon[k] = v
end
end
local function cleanName(S, name, max)
local nameClean = require("Ops").nicknameSanitize(S, tostring(name or ""))
local chars = {}
for ch in nameClean:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
if #chars < max then
chars[#chars + 1] = ch
end
end
return table.concat(chars)
end
function A.repair(S, original)
local Ops, L = require("Ops"), require("Legality")
if type(original) ~= "table" or not Ops.speciesUsable(S, original.species or original.speciesId) then
return nil, "Choose a known species first"
end
local mon, g = Copy(original), Gen.ofState(S)
local tables = g == 3 and { "ivs", "evs", "moves", "pp", "ppBonuses", "contest", "stats" }
or { "dvs", "statExp", "moves", "pp", "ppBonuses", "stats" }
for _, key in ipairs(tables) do
if type(mon[key]) ~= "table" then
mon[key] = {}
end
end
mon.level = clamp(mon.level, 1, 100)
mon.nickname = cleanName(S, mon.nickname, 10)
for _, d in ipairs(P.all(S)) do
local v = P.get(mon, d)
if d.text then
v = cleanName(S, v, d.max)
elseif d.toggle then
v = v == true or v == 1
else
v = clamp(v, d.lo, d.hi)
if d.values and P.parse(d, v) == nil then
v = d.default or d.choices[1][1]
end
end
P.write(mon, d, v)
end
if mon.happiness ~= nil then
mon.happiness = clamp(mon.happiness, 0, 255)
end
if mon.friendship ~= nil then
mon.friendship = clamp(mon.friendship, 0, 255)
end
if g == 3 then
local remaining = 510
for _, k in ipairs(Limits.EV_KEYS) do
mon.ivs[k] = clamp(mon.ivs[k], 0, 31)
mon.evs[k] = clamp(mon.evs[k], 0, math.min(255, remaining))
remaining = remaining - mon.evs[k]
end
local Pokemon = require("src.core.game3.pokemon")
local pair = Pokemon.abilities(mon.species or mon.speciesId)
mon.abilityNum = pair[2] and pair[2] ~= 0 and mon.personality % 2 or 0
mon.ability, mon.abilityId, mon.isShiny = pair[mon.abilityNum + 1], pair[mon.abilityNum + 1], nil
mon.metLevel = math.min(mon.metLevel or 0, mon.level)
if mon.egg or mon.isEgg then
mon.level, mon.metLevel, mon.language, mon.pokeball = 5, 0, 1, 4
for _, d in ipairs(P.contest) do
P.write(mon, d, 0)
end
end
local bit = require("bit")
local word = P.get(mon, { ribbon = true, shift = 0, width = 32 })
word = bit.band(clamp(word, 0, 4294967295), 0x87FFFFFF)
mon.ribbons = word < 0 and word + 4294967296 or word
else
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
mon.dvs[k] = clamp(mon.dvs[k], 0, 15)
end
M.syncHpDv(mon.dvs)
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
mon.statExp[k] = clamp(mon.statExp[k], 0, 65535)
end
if g == 2 then
M.setDv(S.data, mon, "attack", mon.dvs.attack, g)
end
end
local strain = math.floor(clamp(mon.pokerus, 0, 255) / 16)
if g == 2 then
strain = math.min(8, strain)
end
local days = strain == 0 and 0 or math.min(clamp(mon.pokerus, 0, 255) % 16, strain % 4 + 1)
if mon.pokerus ~= nil then
mon.pokerus = strain * 16 + days
end
local held = mon.heldItem or mon.item
if held and held ~= 0 and held ~= "NONE" and (g == 1 or not Ops.itemHoldable(S, held)) then
mon.heldItem, mon.item = nil, nil
end
local status = { SLP = true, PSN = true, BRN = true, FRZ = true, PAR = true, TOX = g ~= 1 }
if type(mon.status) == "number" then
local allowed = { [0] = true, [8] = true, [16] = true, [32] = true, [64] = true, [128] = true }
if not allowed[mon.status] and not (mon.status >= 1 and mon.status <= 7) then
mon.status = nil
end
elseif not status[mon.status] then
mon.status = nil
end
mon.sleep = mon.status == "SLP" and clamp(mon.sleep, 1, 7) or nil
local seen, moves = {}, {}
for slot = 1, 4 do
local mv = mon.moves[slot]
local id = type(mv) == "table" and (mv.moveId or mv.id) or mv
if id and id ~= 0 and Ops.moveUsable(S, id) and not seen[id] then
moves[slot], seen[id] = mv, true
end
end
mon.moves = moves
mon.ppBonusesPacked = clamp(mon.ppBonusesPacked, 0, 255)
if next(moves) == nil and not (mon.egg or mon.isEgg) then
local temp = { data = S.data, save = S.save, version = S.version }
Ops.resetMoves(temp, mon)
end
for slot = 1, 4 do
local mv = mon.moves[slot]
if mv then
local ups = clamp(M.getPpUps(mon, slot), 0, 3)
if mon.egg or mon.isEgg or M.getBasePp(S.data, mon, slot) == 1 then
ups = 0
end
M.setPpUps(S.data, mon, slot, ups, g)
M.setPp(S.data, mon, slot, clamp(type(mv) == "table" and mv.pp or mon.pp[slot], 0, 255), g)
else
M.clearMove(mon, slot)
mon.pp[slot] = 0
M.setPpUps(S.data, mon, slot, 0, g)
end
end
local lo = Limits.expAt(S, mon, mon.level)
local hi = mon.level == 100 and lo or Limits.expAt(S, mon, mon.level + 1) - 1
local exp = clamp(Gen.exp(mon), lo, hi)
if g == 2 then
mon.experience = exp
else
mon.exp = exp
end
if mon.exp ~= nil then
mon.exp = exp
end
if mon.experience ~= nil then
mon.experience = exp
end
M.recalc(S.data, mon, g)
mon.hp = clamp(mon.hp, 0, mon.maxHp or mon.stats.hp)
local report = L.mon(S, mon)
if report.errors > 0 then
for _, check in ipairs(report.checks) do
if check.kind == "error" then
return nil, check.message
end
end
end
return mon
end
function A.fixMon(S, mon)
local Ops, L = require("Ops"), require("Legality")
if L.mon(S, mon).errors == 0 then
return Ops.say(S, "No property errors to fix")
end
local staged, err = A.repair(S, mon)
if not staged then
return Ops.say(S, err)
end
replace(mon, staged)
return Ops.mark(S, "Fixed property errors. Check origin warnings separately.")
end
function A.fixAll(S)
local fixed, left = 0, 0
for _, entry in ipairs(require("Legality").save(S).entries) do
if entry.report.errors > 0 then
local staged = A.repair(S, entry.mon)
if staged then
replace(entry.mon, staged)
fixed = fixed + 1
else
left = left + 1
end
end
end
local msg = "Fixed " .. fixed .. " Pokémon"
if left > 0 then
msg = msg .. "; " .. left .. " need a manual choice"
end
if fixed == 0 then
return require("Ops").say(S, left > 0 and msg or "No property errors to fix")
end
return require("Ops").mark(S, msg .. ". Origin warnings stay for review.")
end
function A.maxMon(S, mon)
local Ops, g = require("Ops"), Gen.ofState(S)
local staged, err = A.repair(S, mon)
if not staged then
return Ops.say(S, err)
end
if staged.egg or staged.isEgg then
return Ops.say(S, "Hatch the egg before maxing it out")
end
M.setLevel(S.data, staged, 100, g)
M.setHappiness(S.data, staged, 255, g)
if g == 3 then
M.maxIvs(S.data, staged, g)
-- Keep the existing spread; spend unused points on the strongest stats.
local stats = staged.stats or {}
local map = {
hp = "hp",
atk = "attack",
def = "defense",
spa = "specialAttack",
spd = "specialDefense",
spe = "speed",
}
local keys = Copy(Limits.EV_KEYS)
table.sort(keys, function(a, b)
local av, bv = stats[map[a]] or 0, stats[map[b]] or 0
return av ~= bv and av > bv or av == bv and a < b
end)
for _, k in ipairs(keys) do
M.setEv(S.data, staged, k, math.max(staged.evs[k] or 0, 252), g)
end
else
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
M.setDv(S.data, staged, k, 15, g)
end
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
staged.statExp[k] = 65535
end
end
M.maxAllPpUps(S.data, staged, g)
M.recalc(S.data, staged, g)
staged.hp, staged.status = staged.maxHp or staged.stats.hp, nil
staged.sleep, staged.toxicCounter = nil, nil
local result = require("Legality").mon(S, staged)
if result.errors > 0 then
for _, check in ipairs(result.checks) do
if check.kind == "error" then
return Ops.say(S, "Max refused: " .. check.message)
end
end
end
replace(mon, staged)
return Ops.mark(S, "Maxed level, stats, friendship and PP; fully healed")
end
-- pokefirered/src/wild_encounter.c:49 sUnownLetterSlots, by chamber.
local UNOWN_LETTERS = {
MAPSEC_MONEAN_CHAMBER = { 0, 27 },
MAPSEC_LIPTOO_CHAMBER = { 2, 3, 7, 20, 14 },
MAPSEC_WEEPTH_CHAMBER = { 13, 18, 8, 4 },
MAPSEC_DILFORD_CHAMBER = { 15, 11, 9, 17, 16 },
MAPSEC_SCUFIB_CHAMBER = { 24, 19, 6, 5, 10 },
MAPSEC_RIXY_CHAMBER = { 21, 22, 23, 12, 1 },
MAPSEC_VIAPOIS_CHAMBER = { 25, 26 },
}
local function random(lo, hi)
return math.random(lo, hi)
end
function A.encounters(S)
if S._randomEncounters then
return S._randomEncounters
end
local candidates, g = {}, Gen.ofState(S)
local tables = S.data.gen2Encounters or S.data.encounters or {}
if g == 2 and (tables.grass or tables.water) then
tables = { grass = tables.grass, water = tables.water }
end
if g == 3 then
local E = require("src.core.game3.encounters")
E.ensureLoaded()
tables = {}
for map in pairs(S.data.maps or {}) do
if type(map) == "string" then
tables[map] = E.tableFor(map)
end
end
end
local function walk(t, map, time)
if type(t) ~= "table" then
return
end
local sp = t.species or t.pokemon
local lo = tonumber(t.minLevel or t.level)
local hi = tonumber(t.maxLevel or t.level)
if sp and lo and hi and require("Ops").speciesUsable(S, sp) then
local def = map and S.data.maps and S.data.maps[map]
local location = def and tonumber(g == 3 and def.regionMapSectionId or def.landmark)
if (g ~= 3 and not Gen.hasCaughtData(S.save, S.version)) or location then
candidates[#candidates + 1] =
{ species = sp, lo = lo, hi = hi, map = map, location = location, time = time }
end
else
for key, v in pairs(t) do
local nextMap = type(key) == "string" and S.data.maps and S.data.maps[key] and key or map
local nextTime = ({ MORN = 1, DAY = 2, NITE = 3 })[key] or time
walk(v, nextMap, nextTime)
end
end
end
walk(tables)
S._randomEncounters = candidates
return candidates
end
function A.randomize(S, mon)
local Ops, g = require("Ops"), Gen.ofState(S)
local candidates = A.encounters(S)
if #candidates == 0 then
return Ops.say(S, "This game's encounter data is unavailable")
end
local row = candidates[random(1, #candidates)]
local staged = M.create(S.data, row.species, random(row.lo, row.hi), g)
staged.ot = S.save.player and S.save.player.name or S.save.name or "RED"
staged.otName = staged.ot
staged.otId = S.save.player and S.save.player.id or S.save.trainerId or 0
if g == 3 then
-- Independent stream: editing never advances the running game's RNG.
local R = require("src.core.game3.rng")
local seed = random(0, 65535) + random(0, 65535) * 65536
local function next16()
seed = (R.mulU32(seed, 1103515245) + 24691) % 4294967296
return math.floor(seed / 65536)
end
local v = Gen.versionOf(S.save, S.version)
local isRse = require("src.core.GameVersion").layout(v) == "rse"
local constants = require("src.core.game3.constants").of(isRse and "emerald" or "firered")
local sections = constants.region_map_sections.byName
local safari = row.location == sections.MAPSEC_SAFARI_ZONE or row.location == sections.MAPSEC_KANTO_SAFARI_ZONE
if safari then
staged.pokeball = 5
end
if tonumber(staged.species) == constants:id("species", "SPECIES_UNOWN") and not isRse then
local letters = UNOWN_LETTERS[constants:name("region_map_sections", row.location, "MAPSEC_")]
if not letters then
return Ops.say(S, "No verified Unown form for this location")
end
local letter = letters[random(1, #letters)]
-- Unown uses high-half first; normal Random32 uses low-half first.
repeat
local high, low = next16(), next16()
staged.personality = low + high * 65536
until require("src.core.game3.pokemon").unownLetter(staged.personality) == letter
else
-- GenerateWildMon -> CreateMonWithNature -> CreateBoxMon. Preserve
-- the native nature rejection loop and its following two IV draws.
if isRse and safari then
next16()
end
local nature = next16() % 25
repeat
local low, high = next16(), next16()
staged.personality = low + high * 65536
until staged.personality % 25 == nature
end
local iv1, iv2 = next16(), next16()
staged.ivs = {
hp = iv1 % 32,
atk = math.floor(iv1 / 32) % 32,
def = math.floor(iv1 / 1024) % 32,
spe = iv2 % 32,
spa = math.floor(iv2 / 32) % 32,
spd = math.floor(iv2 / 1024) % 32,
}
staged.abilityNum = nil
staged.otSecretId = S.save.secretId or 0
staged.otGender = S.save.gender or 0
staged.language, staged.metLocation, staged.metLevel = 2, row.location, staged.level
staged.metGame = require("src.core.GameVersion").gameCode(Gen.versionOf(S.save, S.version))
elseif g == 2 and Gen.hasCaughtData(S.save, S.version) then
staged.caughtLevel = math.min(63, staged.level)
staged.caughtLocation, staged.caughtTime = row.location, row.time or 2
staged.caughtByGender = Gen.playerGender(S.save) == "female" and "girl" or "boy"
end
M.recalc(S.data, staged, g)
staged.hp = staged.maxHp or staged.stats.hp
local report = require("Legality").mon(S, staged)
if report.errors > 0 then
return Ops.say(S, "Randomization refused: generated values failed checks")
end
replace(mon, staged)
return Ops.mark(S, "Randomized " .. Ops.monName(S, mon) .. " from this game's wild encounters")
end
return A