Files
gen1recomp/tools/save-editor/panels/MonEditor.lua
T
bryanthaboi ca4d3d283c Add Pokemon Crystal as a sixth supported version
Crystal boots from a user-supplied ROM, imports a full cache and is playable:
copyright, the Crystal intro movie, the animated title, gender select, Oak,
and out into Johto. 122 of the cart's 169 script specials are implemented.

Import and data
- tools/make_crystal_manifest.py derives the manifest by importing
  make_gold_manifest as a library, with three additive keyword seams. Gold and
  Silver still regenerate byte-identical, which is the standing requirement for
  touching that generator.
- crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta:
  Crystal renames the credits mons, splits the trainer card, Pokegear and
  pack-pal blocks by gender, and replaces the intro and title outright.
- Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the
  badge block sits one higher) and unownCharmap (the main charmap parser stops
  at the first newcharmap so the two cannot contaminate each other).

Extractor
- RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK
  0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole,
  seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim
  steps with per-row degrade, and the column-major trainer card portraits.
- New: animated front sprites (frames, bitmasks, play and idle scripts), the
  Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal
  intro and title via src/import/CrystalMovie.lua.

Engine
- GameVersion gains engine(id) and fixes(id). Gold and Silver keep their
  original bugs where the bug is not hardware dependent; Crystal gets the fixes
  Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the
  Reflect and Light Screen defence overflow.
- Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins
  of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower
  including the wInBattleTowerBattle badge-boost guard.
- Kris and the gender flag, animated fronts in battle and the summary screen,
  and mon caught data.

Verification
- Every extracted asset is pixel-compared against pret's own source PNGs.
- Gold caches are byte-identical before and after, file for file.
- New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
2026-08-23 12:10:28 -04:00

496 lines
20 KiB
Lua

-- The mon inspector: species, level, DVs and moves for whatever S.editingMon
-- points at (a party slot or a box slot), all recalculated through MonOps so
-- stats stay in sync with the Gen1 formulas.
--
-- This used to be a modal overlay floating over the party list, which hid the
-- roster you were comparing against. It is now a permanent right-hand column
-- the Party and Boxes panels dock into (rule 1 of the design spec): the list
-- stays visible while you edit, and Escape clears the selection rather than
-- "closing a window".
--
-- #715 reflow: the inspector used to shrink its stat tiles and DV/move rows
-- against a vertical budget, and past a point the rows still ran over the
-- action buttons. Sizes are fixed at readable values now; when the card is
-- too short for them the whole body scrolls (Kit.scrollPixels), and when it
-- is too narrow for the DV | moves split the two columns stack. The clip
-- over the card doubles as the hit fence, so a control scrolled out of view
-- cannot take a stray tap.
local Theme = require("Theme")
local Ops = require("Ops")
local PAL = Theme.PAL
local Gen = require("Gen")
local MonEditor = {}
local DV_KEYS = { "attack", "defense", "speed", "special" }
local STAT_KEYS_G1 = {
{ key = "HP", field = "hp" },
{ key = "ATK", field = "attack" },
{ key = "DEF", field = "defense" },
{ key = "SPD", field = "speed" },
{ key = "SPC", field = "special" },
}
local STAT_KEYS_G2 = {
{ key = "HP", field = "hp" },
{ key = "ATK", field = "attack" },
{ key = "DEF", field = "defense" },
{ key = "SPA", field = "specialAttack" },
{ key = "SPD", field = "specialDefense" },
{ key = "SPE", field = "speed" },
}
-- Front sprites are read straight off the generated cache. One image per
-- species, cached for the process: the old panel called newImage every frame,
-- which re-decoded a PNG sixty times a second.
local spriteCache = {}
function MonEditor.sprite(S, species)
if spriteCache[species] ~= nil then return spriteCache[species] or nil end
local def = S.data.pokemon[species]
local path = def and def.spriteFront
if not path or not love.graphics.newImage then
spriteCache[species] = false
return nil
end
local ok, img = pcall(love.graphics.newImage, path)
spriteCache[species] = ok and img or false
return ok and img or nil
end
-- Draw a species sprite fitted into a box, or a dashed placeholder when the
-- cache has no art for it (a modded species, or a headless run).
function MonEditor.drawSprite(S, Kit, species, x, y, size)
local img = MonEditor.sprite(S, species)
if img and love.graphics.draw and img.getDimensions then
local iw, ih = img:getDimensions()
if iw > 0 and ih > 0 then
local scale = math.min(size / iw, size / ih)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(img, x + (size - iw * scale) / 2,
y + (size - ih * scale) / 2, 0, scale, scale)
return
end
end
Theme.col(PAL.blue, 0.1)
love.graphics.rectangle("fill", x, y, size, size, 8 * Kit.scale, 8 * Kit.scale)
Theme.col(PAL.cardBorder, 0.35)
Theme.dashed(x, y, size, size, 8 * Kit.scale, 5 * Kit.scale, 4 * Kit.scale)
Kit.textCenter("micro", (species or "?"):sub(1, 3), x,
y + size / 2 - Kit.textHeight("micro") / 2, size, PAL.muted)
end
-- Bars are scaled against 400 so a Lv100 legend fills roughly three quarters
-- and the differences between mons stay legible.
local STAT_SCALE = 400
-- The -5 -1 [Lv] +1 +5 stepper row plus the EXP readout, at (lx0, ly).
local function drawLevelRow(S, Kit, mon, lx0, ly)
local s = Kit.scale
local lh = 28 * s
Kit.caption(lx0, ly + (lh - Kit.textHeight("caption")) / 2, "LEVEL")
local lx = lx0 + 52 * s
local bw = 40 * s
for _, d in ipairs({ { "-5", -5 }, { "-1", -1 } }) do
if Kit.stepper(lx, ly, bw, lh, d[1], { font = "small", radius = 7 * s }) then
Ops.setLevel(S, mon, mon.level + d[2])
end
lx = lx + bw + 8 * s
end
Kit.textCenter("monoBig", tostring(mon.level), lx,
ly + (lh - Kit.textHeight("monoBig")) / 2, 58 * s, PAL.heading)
lx = lx + 58 * s + 8 * s
for _, d in ipairs({ { "+1", 1 }, { "+5", 5 } }) do
if Kit.stepper(lx, ly, bw, lh, d[1], { font = "small", radius = 7 * s }) then
Ops.setLevel(S, mon, mon.level + d[2])
end
lx = lx + bw + 8 * s
end
Kit.text("mono", ("EXP %d"):format(Gen.exp(mon)), lx + 6 * s,
ly + (lh - Kit.textHeight("mono")) / 2, PAL.muted)
return lh
end
-- Everything the level row needs in width, for the "does it fit beside the
-- sprite" decision.
local function levelRowWidth(Kit, mon)
local s = Kit.scale
return 52 * s + 2 * (40 * s + 8 * s) + 58 * s + 8 * s + 2 * (40 * s + 8 * s)
+ 6 * s + Kit.textWidth("mono", ("EXP %d"):format(Gen.exp(mon)))
end
local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
local s = Kit.scale
for i, key in ipairs(DV_KEYS) do
local ry = rowY + (i - 1) * (rowH + rowGap)
Theme.row(cx, ry, colW, rowH, 10 * s, 0.6)
local v = mon.dvs[key] or 0
Kit.text("tiny", key:upper(), cx + 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.muted)
local btn = 26 * s
local btnX = cx + colW - 10 * s - 3 * btn - 18 * s
local meterX = cx + 66 * s
local meterW = math.max(20 * s, btnX - meterX - 34 * s)
Kit.meter(meterX, ry + (rowH - 8 * s) / 2, meterW, 8 * s, v / 15 * 100,
v >= 15 and PAL.green or (v >= 10 and PAL.blue or PAL.steel))
Kit.textRight("monoRow", tostring(v), meterX + meterW + 28 * s,
ry + (rowH - Kit.textHeight("monoRow")) / 2, PAL.heading)
if Kit.stepper(btnX, ry + (rowH - btn) / 2, btn, btn, "-") then
Ops.setDv(S, mon, key, v - 1)
end
if Kit.stepper(btnX + btn + 6 * s, ry + (rowH - btn) / 2, btn, btn, "+") then
Ops.setDv(S, mon, key, v + 1)
end
if Kit.button(btnX + 2 * btn + 12 * s, ry + (rowH - btn) / 2, btn, btn,
"15", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.setDv(S, mon, key, 15)
end
end
end
-- engine/pokemon/caught_data.asm:168-199
local CAUGHT_BY = { { "-", "none" }, { "BOY", "boy" }, { "GIRL", "girl" } }
local function drawCaughtRows(S, Kit, mon, cx, y, inner, row)
local s = Kit.scale
local chipH = 22 * s
local gap = 6 * s
local tinyH = Kit.textHeight("tiny")
Kit.text("tiny", "CAUGHT", cx, y + (row - tinyH) / 2, PAL.caption)
local timeX = cx + 62 * s
local timeW = math.max(34 * s, (cx + inner - timeX - 3 * gap) / 4)
for i, label in ipairs(Ops.CAUGHT_TIMES) do
if Kit.chip(timeX + (i - 1) * (timeW + gap), y + (row - chipH) / 2,
timeW, chipH, label, (mon.caughtTime or 0) == i - 1, PAL.blue, PAL.steel) then
Ops.setCaughtTime(S, mon, i - 1)
end
end
y = y + row + gap
local btn = 24 * s
local lvX = cx + inner - 2 * btn - gap
if Kit.stepper(lvX, y + (row - btn) / 2, btn, btn, "-", { font = "small" }) then
Ops.setCaughtLevel(S, mon, (mon.caughtLevel or 0) - 1)
end
if Kit.stepper(lvX + btn + gap, y + (row - btn) / 2, btn, btn, "+",
{ font = "small" }) then
Ops.setCaughtLevel(S, mon, (mon.caughtLevel or 0) + 1)
end
Kit.textRight("tiny", ("MET LV %d"):format(mon.caughtLevel or 0), lvX - 10 * s,
y + (row - tinyH) / 2, PAL.text)
Kit.text("tiny", "OT", cx, y + (row - tinyH) / 2, PAL.caption)
local otX = cx + 26 * s
local otW = math.max(30 * s, (inner * 0.42 - 26 * s - 2 * gap) / 3)
for i, pair in ipairs(CAUGHT_BY) do
if Kit.chip(otX + (i - 1) * (otW + gap), y + (row - chipH) / 2, otW, chipH,
pair[1], (mon.caughtByGender or "none") == pair[2], PAL.blue, PAL.steel) then
Ops.setCaughtByGender(S, mon, pair[2])
end
end
y = y + row + gap
local whereX = cx + inner - 3 * btn - 2 * gap
if Kit.stepper(whereX, y + (row - btn) / 2, btn, btn, "-", { font = "small" }) then
Ops.setCaughtLocation(S, mon, (mon.caughtLocation or 0) - 1)
end
if Kit.stepper(whereX + btn + gap, y + (row - btn) / 2, btn, btn, "+",
{ font = "small" }) then
Ops.setCaughtLocation(S, mon, (mon.caughtLocation or 0) + 1)
end
if Kit.button(whereX + 2 * (btn + gap), y + (row - btn) / 2, btn, btn, "0",
{ kind = "danger", font = "micro", radius = 6 * s }) then
Ops.setCaughtLocation(S, mon, 0)
end
local where = ("WHERE %s"):format(Gen.landmarkName(S.data, mon.caughtLocation or 0))
Kit.text("tiny", Kit.ellipsize("tiny", where, whereX - 10 * s - cx), cx,
y + (row - tinyH) / 2, PAL.text)
return y + row + gap
end
local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
local s = Kit.scale
for slot = 1, 4 do
local ry = rowY + (slot - 1) * (rowH + rowGap)
Theme.row(rightX, ry, colW, rowH, 10 * s, 0.6)
local mv = mon.moves and mon.moves[slot]
local clear = 24 * s
local clearX = rightX + colW - 10 * s - clear
local ppText = mv and ("PP %d"):format(mv.pp or 0) or ""
local ppW = Kit.textWidth("tiny", ppText)
Kit.text("mono", tostring(slot), rightX + 10 * s,
ry + (rowH - Kit.textHeight("mono")) / 2, PAL.faint)
local nameX = rightX + 28 * s
local nameW2 = math.max(20 * s, clearX - 12 * s - ppW - nameX)
Kit.text("monoRow", Kit.ellipsize("monoRow", mv and mv.id or "-- --", nameW2),
nameX, ry + (rowH - Kit.textHeight("monoRow")) / 2,
mv and PAL.text or PAL.faint)
Kit.textRight("tiny", ppText, clearX - 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.caption)
-- the row body cycles, the x empties: two targets, no modal picker
if Kit.press(rightX, ry, clearX - rightX - 4 * s, rowH) then
Ops.cycleMove(S, mon, slot)
end
if Kit.button(clearX, ry + (rowH - clear) / 2, clear, clear, "x",
{ kind = "danger", font = "tiny", radius = 6 * s }) then
Ops.clearMove(S, mon, slot)
end
end
end
function MonEditor.draw(S, Kit, x, y, w, h)
local s = Kit.scale
Kit.card(x, y, w, h)
local mon = S.editingMon
local pad = 18 * s
if not mon then
-- The inspector column is always drawn, so it explains itself rather
-- than collapsing and reflowing the panel underneath it.
local tw = math.min(w - 40 * s, 340 * s)
Kit.textCenter("button",
"Pick a slot on the left to inspect it. Every change here re-runs the " ..
"stat formulas, so HP and stats stay legal.",
x + (w - tw) / 2, y + h / 2 - Kit.textHeight("button"), tw, PAL.muted)
return
end
local def = S.data.pokemon[mon.species]
local inner = w - 2 * pad
local capH = Kit.textHeight("caption")
local titleH = Kit.textHeight("title")
-- Reflow decisions, all against real pixels (#715): the DV | moves split
-- needs ~470px of card interior; below that the two stacks go one above
-- the other. A narrow card also drops the sprite to 64px so the header
-- text keeps room.
local narrow = inner < 470 * s
local sprite = (narrow and 64 or 96) * s
local rowH = 30 * s
local rowGap = 8 * s
local cellH = 52 * s
local actH = 34 * s
local hw = inner - sprite - 18 * s
local levelInHeader = hw >= levelRowWidth(Kit, mon)
local headerH
if levelInHeader then
headerH = math.max(sprite, titleH + 14 * s + 28 * s)
else
-- the level row does not fit beside the sprite: it drops below the
-- header block at full card width instead of painting over the sprite
headerH = math.max(sprite, titleH) + 12 * s + 28 * s
end
local colRowsH = 4 * (rowH + rowGap) - rowGap
local colsH
if narrow then
colsH = (capH + 10 * s + colRowsH) * 2 + 14 * s + 10 * s + actH
else
colsH = capH + 10 * s + colRowsH + 12 * s + actH
end
-- the nickname section: a caption line (with the Clear button on it) plus
-- the field + Set row
local extraH = 0
if Gen.ofState(S) == 2 then extraH = 88 * s end
if Gen.hasCaughtData(S.save, S.version) then extraH = extraH + 102 * s end
local nickFieldH = 30 * s
local contentH = pad + headerH + 18 * s
+ capH + 10 * s + nickFieldH + 18 * s
+ capH + 10 * s + cellH + 18 * s
+ extraH
+ colsH + pad
-- Called before the widgets so this frame already draws at the updated
-- offset; any list-free card body is fair game for the drag (#715).
S.inspectorScroll = Kit.scrollPixels(x, y, w, h, S.inspectorScroll, contentH)
Kit.pushClip(x, y, w, h)
local cx = x + pad
local cy = y + pad - S.inspectorScroll
-- ---------------------------------------------------------- header row
MonEditor.drawSprite(S, Kit, mon.species, cx, cy, sprite)
local hx = cx + sprite + 18 * s
-- One control instead of a pair of arrows: cycling walked the catalog an
-- entry at a time (151 taps to cross the dex) and ran a full MonOps
-- recalculation on every step, including on records the Gen1 formulas
-- cannot use, which is what crashed the editor (#541). This opens the
-- searchable picker; the species name itself is a second, larger target.
local pickH = 30 * s
local pickW = math.min(150 * s, math.max(90 * s, hw * 0.6))
local px = hx + hw - pickW
local py = cy + (titleH - pickH) / 2
-- the species name yields to the button instead of running under it (#715)
local name = Kit.ellipsize("title", mon.species, math.max(40 * s, px - hx - 12 * s))
Kit.text("title", name, hx, cy, PAL.heading)
local nameW = Kit.textWidth("title", name)
if nameW + Kit.textWidth("tiny", "#000") + 12 * s < px - hx - 12 * s then
Kit.text("tiny", ("#%03d"):format(def and def.dex or 0), hx + nameW + 12 * s,
cy + titleH - Kit.textHeight("tiny") - 2 * s, PAL.caption)
end
local openPicker = Kit.button(px, py, pickW, pickH, "Change species",
{ kind = "accent", font = "small", radius = 8 * s })
if not openPicker then
openPicker = Kit.press(hx, cy, math.max(0, px - hx - 10 * s), titleH)
end
if openPicker then Ops.openSpeciesPicker(S, Kit) end
-- level stepper: -5 -1 [Lv] +1 +5, matching MonOps.setLevel's 1..100 clamp
if levelInHeader then
drawLevelRow(S, Kit, mon, hx, cy + titleH + 14 * s)
else
drawLevelRow(S, Kit, mon, cx, cy + math.max(sprite, titleH) + 12 * s)
end
-- ---------------------------------------------------------- nickname
-- Editing the field is a draft (S.nicknameDraft) held on the mon it belongs
-- to; Set / Enter commit it through Ops.setNickname, which clears on an
-- empty value, and Clear goes through Ops.clearNickname. The draft resets
-- when the selection moves so one mon's typing can never leak onto another.
local nickY = cy + headerH + 18 * s
Kit.caption(cx, nickY, "NICKNAME")
local clearW = 74 * s
local clearH = 24 * s
if Kit.button(cx + inner - clearW, nickY + (capH - clearH) / 2, clearW, clearH,
"Clear", { kind = "danger", font = "micro", radius = 6 * s }) then
Ops.clearNickname(S, mon)
S.nicknameDraft = ""
end
local fieldY = nickY + capH + 10 * s
local setW = 64 * s
local fieldW = inner - setW - 10 * s
if S.nicknameMon ~= mon then
local switching = S.nicknameMon ~= nil
S.nicknameMon = mon
S.nicknameDraft = mon.nickname or ""
-- a still-focused field would keep appending keystrokes to the newly
-- selected mon; the selection move counts as leaving the field. The
-- first sync (nicknameMon starts nil) never blurs: the species picker
-- owns focus when it opens, and blurring there drops the player's typing.
if switching and Kit.focus == "mon-nickname" then Kit.blur() end
end
S.nicknameDraft = Kit.textfield("mon-nickname", cx, fieldY, fieldW, nickFieldH,
S.nicknameDraft or "", "no nickname",
{ sanitize = function(value) return Ops.nicknameSanitize(S, value) end })
if Kit.button(cx + fieldW + 10 * s, fieldY, setW, nickFieldH, "Set",
{ kind = "accent", font = "small", radius = 8 * s }) then
if Ops.setNickname(S, mon, S.nicknameDraft) then
S.nicknameDraft = mon.nickname or ""
end
end
-- ------------------------------------------------------- derived stats
local statsY = nickY + capH + 10 * s + nickFieldH + 18 * s
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
statsY = statsY + capH + 10 * s
local STAT_KEYS = Gen.ofState(S) == 2 and STAT_KEYS_G2 or STAT_KEYS_G1
local gap = 12 * s
local cellW = (inner - gap * (#STAT_KEYS - 1)) / #STAT_KEYS
for i, st in ipairs(STAT_KEYS) do
local bx = cx + (i - 1) * (cellW + gap)
Theme.row(bx, statsY, cellW, cellH, 10 * s, 0.6)
local value = (mon.stats and mon.stats[st.field]) or 0
Kit.text("micro", st.key, bx + 12 * s, statsY + 8 * s, PAL.caption)
Kit.text("stat", tostring(value), bx + 12 * s,
statsY + 8 * s + Kit.textHeight("micro") + 2 * s, PAL.heading)
Kit.meter(bx + 12 * s, statsY + cellH - 12 * s, cellW - 24 * s, 5 * s,
value / STAT_SCALE * 100, PAL.blue)
end
-- --------------------------------------------------- DVs | moves split
local colY = statsY + cellH + 18 * s
if Gen.ofState(S) == 2 then
local extraY = colY
Kit.caption(cx, extraY, Gen.editionLabel(S.save, S.version))
extraY = extraY + capH + 8 * s
local row = 28 * s
Kit.text("tiny", "HELD " .. tostring(mon.item or "none"), cx, extraY, PAL.text)
if Kit.button(cx + inner - 70 * s, extraY, 70 * s, row, "Clear item",
{ kind = "danger", font = "tiny", radius = 6 * s }) then
Ops.setHeldItem(S, mon, nil)
end
extraY = extraY + row + 6 * s
Kit.text("tiny", ("HAPPINESS %d"):format(mon.happiness or 0), cx, extraY, PAL.text)
if Kit.stepper(cx + 140 * s, extraY, 28 * s, row, "-", { font = "small" }) then
Ops.setHappiness(S, mon, (mon.happiness or 0) - 10)
end
if Kit.stepper(cx + 174 * s, extraY, 28 * s, row, "+", { font = "small" }) then
Ops.setHappiness(S, mon, (mon.happiness or 0) + 10)
end
Kit.text("tiny", ("PKRS %d"):format(mon.pokerus or 0), cx + 220 * s, extraY, PAL.text)
if Kit.stepper(cx + 300 * s, extraY, 28 * s, row, "-", { font = "small" }) then
Ops.setPokerus(S, mon, (mon.pokerus or 0) - 1)
end
if Kit.stepper(cx + 334 * s, extraY, 28 * s, row, "+", { font = "small" }) then
Ops.setPokerus(S, mon, (mon.pokerus or 0) + 1)
end
extraY = extraY + row + 4 * s
local bits = {}
if mon.gender then bits[#bits + 1] = mon.gender end
if mon.shiny then bits[#bits + 1] = "shiny" end
if mon.unownLetter then bits[#bits + 1] = "Unown " .. tostring(mon.unownLetter) end
Kit.text("tiny", table.concat(bits, " ") ~= "" and table.concat(bits, " ")
or "gender/shiny follow DVs", cx, extraY, PAL.caption)
extraY = extraY + 22 * s
if Gen.hasCaughtData(S.save, S.version) then
extraY = drawCaughtRows(S, Kit, mon, cx, extraY, inner, row)
end
colY = extraY
end
if narrow then
-- stacked: DVs first, then moves, then the two actions side by side at
-- full width (#715)
Kit.caption(cx, colY, "DVs")
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format(mon.dvs.hp or 0),
cx + inner, colY, PAL.caption)
local rowY = colY + capH + 10 * s
drawDvRows(S, Kit, mon, cx, rowY, inner, rowH, rowGap)
local movesY = rowY + colRowsH + 14 * s
Kit.caption(cx, movesY, "MOVES")
Kit.textRight("tiny", "click a slot to cycle", cx + inner, movesY, PAL.caption)
local mRowY = movesY + capH + 10 * s
drawMoveRows(S, Kit, mon, cx, mRowY, inner, rowH, rowGap)
local actY = mRowY + colRowsH + 10 * s
local actW = (inner - 10 * s) / 2
if Kit.button(cx, actY, actW, actH, "Reset to learnset",
{ font = "small", radius = 9 * s }) then
Ops.resetMoves(S, mon)
end
if Kit.button(cx + actW + 10 * s, actY, actW, actH, "Full heal",
{ kind = "good", font = "small", radius = 9 * s }) then
Ops.healMon(S, mon)
end
else
local colGap = 18 * s
local colW = (inner - colGap) / 2
local rightX = cx + colW + colGap
Kit.caption(cx, colY, "DVs")
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format(mon.dvs.hp or 0),
cx + colW, colY, PAL.caption)
Kit.caption(rightX, colY, "MOVES")
Kit.textRight("tiny", "click a slot to cycle", rightX + colW, colY, PAL.caption)
local rowY = colY + capH + 10 * s
drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
local actY = rowY + colRowsH + 12 * s
local actW = (colW - 10 * s) / 2
if Kit.button(rightX, actY, actW, actH, "Reset to learnset",
{ font = "small", radius = 9 * s }) then
Ops.resetMoves(S, mon)
end
if Kit.button(rightX + actW + 10 * s, actY, actW, actH, "Full heal",
{ kind = "good", font = "small", radius = 9 * s }) then
Ops.healMon(S, mon)
end
end
Kit.popClip()
end
return MonEditor