mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
Merge pull request #1311 from 1Jamie/feat/gold-save-editor
This commit is contained in:
@@ -76,15 +76,6 @@ end
|
||||
local editorHost, editorVersion, editorWindow
|
||||
local closeEditor -- forward declaration: openEditor hands it to the editor
|
||||
|
||||
-- tools/save-editor/ models the Gen 1 save and nothing else: a Gen 2 party row
|
||||
-- carries fields its MonOps and panels have no idea about (dvs, statExp,
|
||||
-- happiness, pokerus, caughtLevel), and SaveIO.save writes the WHOLE table
|
||||
-- back, so a Gold slot opened here comes out in a shape src/core/gen2/Save.lua
|
||||
-- then has to quarantine on the next boot. Refuse by name, the same way the
|
||||
-- .sav paths do (src/save_convert/SaveConvert.lua GEN2_SAV_UNSUPPORTED), so
|
||||
-- Red/Blue/Yellow slots are untouched.
|
||||
local GEN2_NO_EDITOR = { gold = "Pokemon Gold" }
|
||||
|
||||
-- The editor's modules use flat names (require("Kit"), require("Party")), so
|
||||
-- their directories have to be on the require path. It must be
|
||||
-- love.filesystem's path, not package.path: in a packaged build these files
|
||||
@@ -137,11 +128,6 @@ local function openEditor(version, slotId)
|
||||
Importer.saveNotice = Importer.saveNotice or {}
|
||||
Importer.saveNotice[version] = { ok = false, text = text }
|
||||
end
|
||||
local gen2Name = GEN2_NO_EDITOR[version]
|
||||
if gen2Name then
|
||||
refuse(gen2Name .. " uses a Gen 2 save; the save editor does not read one yet.")
|
||||
return
|
||||
end
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local path = SaveData.slotDiskPath(version, slotId)
|
||||
if not path then
|
||||
@@ -320,14 +306,6 @@ function love.load(args)
|
||||
-- cache has to be mounted before the editor's Data:load.
|
||||
if editorMode then
|
||||
local version = os.getenv("POKEPORT_VERSION") or "red"
|
||||
local gen2Name = GEN2_NO_EDITOR[version]
|
||||
if gen2Name then
|
||||
-- No launcher behind this run to carry a notice, so say it and stop
|
||||
-- rather than open a Gen 2 slot on Gen 1 panels.
|
||||
print(gen2Name .. " uses a Gen 2 save; the save editor does not read one yet.")
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
require("src.core.GameVersion").set(version)
|
||||
require("src.import.CacheFs").mountVersion(version)
|
||||
addEditorRequirePath()
|
||||
|
||||
@@ -51,6 +51,12 @@ rm -f "$OUTPUT"
|
||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||
if [ -f "$ROOT/PATCH_NOTES.md" ]; then
|
||||
(cd "$ROOT" && zip -q "$OUTPUT" PATCH_NOTES.md)
|
||||
fi
|
||||
if [ -f "$ROOT/mobile/ios/app-repo.json" ]; then
|
||||
(cd "$ROOT" && zip -q "$OUTPUT" mobile/ios/app-repo.json)
|
||||
fi
|
||||
|
||||
if [ -n "$BUILD_INFO" ]; then
|
||||
[ -f "$BUILD_INFO" ] || fail "missing build-info: $BUILD_INFO"
|
||||
|
||||
@@ -135,6 +135,7 @@ if [ -f data/generated/maps.lua ]; then
|
||||
run_tier "T3 save editor: events + dex" "$LUA" tests/save_editor_task7_tests.lua
|
||||
run_tier "T3 save editor: map browser" "$LUA" tests/save_editor_task8_tests.lua
|
||||
run_tier "T3 save editor: mod awareness" "$LUA" tests/save_editor_mod_tests.lua
|
||||
run_tier "T3 save editor: gold / gen2" "$LUA" tests/save_editor_gen2_tests.lua
|
||||
run_tier "T3 save editor: wheel scrolling" "$LUA" tests/save_editor_wheel_bug595_test.lua
|
||||
run_tier "T3 save editor: pad / NX input" "$LUA" tests/save_editor_pad_input_test.lua
|
||||
run_tier "T5 link (loopback lockstep)" "$LUA" tests/run_link_tests.lua
|
||||
|
||||
@@ -98,10 +98,69 @@ function Mon.stats(baseStats, dvs, level, statExp)
|
||||
}
|
||||
end
|
||||
|
||||
-- The species string is the source of truth. `mon.name` is a copy of that
|
||||
-- species' display name (GetPokemonName), kept so menus can print without a
|
||||
-- Data lookup. It is NOT the nickname: an un-nicknamed mon has nickname nil
|
||||
-- and prints this copy. Changing species without rewriting it leaves the
|
||||
-- previous species' name on the party list and the SUMMARY's top line.
|
||||
function Mon.syncIdentity(mon, data)
|
||||
if type(mon) ~= "table" then return mon end
|
||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||
if not def then return mon end
|
||||
mon.name = def.name or mon.species
|
||||
if def.types then mon.types = def.types end
|
||||
if mon.dvs then
|
||||
mon.gender = Mon.gender(def, mon.dvs,
|
||||
{ species = mon.species, level = mon.level })
|
||||
mon.shiny = Mon.isShiny(mon.dvs,
|
||||
{ species = mon.species, def = def, level = mon.level })
|
||||
if mon.species == Unown.SPECIES then
|
||||
mon.unownLetter = Unown.letterFromDVs(mon.dvs)
|
||||
else
|
||||
mon.unownLetter = nil
|
||||
end
|
||||
end
|
||||
return mon
|
||||
end
|
||||
|
||||
-- Every screen that prints a mon without a Data lookup should go through here:
|
||||
-- nickname if the player set one, otherwise the species display copy `name`.
|
||||
-- Skipping `name` and jumping to `species` is how a swapped mon can still
|
||||
-- read as ABRA on one menu and RAYQUAZA on another.
|
||||
function Mon.displayName(mon)
|
||||
if type(mon) ~= "table" then return "?" end
|
||||
return mon.nickname or mon.name or mon.species or "?"
|
||||
end
|
||||
|
||||
-- Party, boxes, both Day-Care sides, and a pending egg. Editor CONTINUE and
|
||||
-- hydrate have to walk the same set: leaving dayCare.man.mon on the old
|
||||
-- `name` is the ABRA bug in a second closet.
|
||||
function Mon.eachSaveMon(save, fn)
|
||||
if type(save) ~= "table" or type(fn) ~= "function" then return end
|
||||
for _, mon in ipairs(save.party or {}) do fn(mon) end
|
||||
for _, box in pairs(save.boxes or {}) do
|
||||
if type(box) == "table" then
|
||||
for _, mon in ipairs(box) do fn(mon) end
|
||||
end
|
||||
end
|
||||
local dc = save.dayCare
|
||||
if type(dc) == "table" then
|
||||
if dc.man and dc.man.mon then fn(dc.man.mon) end
|
||||
if dc.lady and dc.lady.mon then fn(dc.lady.mon) end
|
||||
if dc.egg then fn(dc.egg) end
|
||||
end
|
||||
if save.daycare and save.daycare.mon then fn(save.daycare.mon) end
|
||||
end
|
||||
|
||||
function Mon.syncSaveIdentity(save, data)
|
||||
Mon.eachSaveMon(save, function(mon) Mon.syncIdentity(mon, data) end)
|
||||
end
|
||||
|
||||
function Mon.refreshStats(mon, data)
|
||||
if type(mon) ~= "table" then return mon end
|
||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||
if not (def and def.baseStats) then return mon end
|
||||
Mon.syncIdentity(mon, data)
|
||||
-- engine/pokemon/move_mon.asm:1402
|
||||
local stats = Mon.stats(def.baseStats, mon.dvs, mon.level or 1, mon.statExp)
|
||||
mon.stats = stats
|
||||
|
||||
+5
-2
@@ -283,6 +283,9 @@ function Game2:continueGame(save)
|
||||
local modsDiff = SaveData.modsDiff(save, activeMods)
|
||||
self.save = save
|
||||
self:adoptSave(save)
|
||||
-- Editor species swaps used to leave mon.name on the previous species.
|
||||
-- CONTINUE rewrites party, boxes, and Day-Care copies from the live record.
|
||||
require("src.battle.gen2.Mon").syncSaveIdentity(save, self.data)
|
||||
-- options.lua wins over anything a save file carries: options are a display
|
||||
-- preference that survives New Game and is edited from the launcher, so a
|
||||
-- save written before they moved out must not drag old values back in.
|
||||
@@ -588,13 +591,13 @@ function Game2:useFieldItem(itemId)
|
||||
end
|
||||
if not allowed then
|
||||
self:say(("%s can't learn %s!"):format(
|
||||
mon.nickname or mon.species or "?", moveName))
|
||||
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||
return
|
||||
end
|
||||
for _, move in ipairs(mon.moves or {}) do
|
||||
if move.id == moveId then
|
||||
self:say(("%s already knows %s!"):format(
|
||||
mon.nickname or mon.species or "?", moveName))
|
||||
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+205
-67
@@ -299,6 +299,11 @@ end
|
||||
|
||||
local CART_DRAG_SLOP = 8
|
||||
local TAU = math.pi * 2
|
||||
-- The 3D mesh is inset inside the hit box so yaw/pitch and the 1.05 hover
|
||||
-- scale cannot climb into the title row (or the gear) on desktop, high-DPI,
|
||||
-- or a portrait phone. Fraction of the shorter side, with a pixel floor.
|
||||
local CART_MESH_PAD = 0.07
|
||||
local CART_MESH_PAD_MIN = 8
|
||||
|
||||
local function cartridgeState(imp, version)
|
||||
imp._cartridge = imp._cartridge or {}
|
||||
@@ -543,8 +548,11 @@ local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action)
|
||||
Theme.A.focus, 2, Theme.cardRadius() + 2)
|
||||
end
|
||||
|
||||
local halfW, halfH = w / 2, h / 2
|
||||
local depth = math.max(8, w * 0.14)
|
||||
local meshPad = math.max(CART_MESH_PAD_MIN,
|
||||
math.floor(math.min(w, h) * CART_MESH_PAD))
|
||||
local halfW = math.max(1, w / 2 - meshPad)
|
||||
local halfH = math.max(1, h / 2 - meshPad)
|
||||
local depth = math.max(8, (halfW * 2) * 0.14)
|
||||
local project = function(px, py, pz)
|
||||
return cartProject(cx + pressX, cy + pressY, yaw, pitch,
|
||||
px * pressedScale, py * pressedScale, pz * pressedScale)
|
||||
@@ -638,14 +646,8 @@ local function modStatusColor(status)
|
||||
end
|
||||
|
||||
-- MODS panel scope row: which game the list is answering for, plus dedicated Profile control (cycle + gear).
|
||||
local function buildModScopeRow(imp, x, y, w, m)
|
||||
local function modScopeOptions(imp)
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
local h = math.max(Kit.tapMin(), math.floor(26 * m.s))
|
||||
local gap = math.floor(6 * m.s)
|
||||
local label = Strings("Show for:")
|
||||
Kit.text("small", label, x, y + (h - Kit.textHeight("small")) / 2, PAL.muted)
|
||||
local cx = x + Kit.textWidth("small", label) + math.floor(10 * m.s)
|
||||
local options = { { id = nil, label = Strings("All games") } }
|
||||
for _, version in ipairs(GameVersion.ORDER) do
|
||||
if imp.ready and imp.ready[version] then
|
||||
@@ -653,6 +655,33 @@ local function buildModScopeRow(imp, x, y, w, m)
|
||||
{ id = version, label = GameVersion.info(version).label }
|
||||
end
|
||||
end
|
||||
return options
|
||||
end
|
||||
|
||||
local function modScopeCurrentLabel(imp, options)
|
||||
for _, opt in ipairs(options) do
|
||||
if imp.modScope == opt.id then return opt.label end
|
||||
end
|
||||
return options[1] and options[1].label or Strings("All games")
|
||||
end
|
||||
|
||||
local function modScopeChipsWidth(options, gap, m)
|
||||
local need = 0
|
||||
for i, opt in ipairs(options) do
|
||||
need = need + Kit.textWidth("micro", opt.label) + math.floor(18 * m.s)
|
||||
if i < #options then need = need + gap end
|
||||
end
|
||||
return need
|
||||
end
|
||||
|
||||
local function buildModScopeRow(imp, x, y, w, m)
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
local h = math.max(Kit.tapMin(), math.floor(26 * m.s))
|
||||
local gap = math.floor(6 * m.s)
|
||||
local label = Strings("Show for:")
|
||||
Kit.text("small", label, x, y + (h - Kit.textHeight("small")) / 2, PAL.muted)
|
||||
local cx = x + Kit.textWidth("small", label) + math.floor(10 * m.s)
|
||||
local options = modScopeOptions(imp)
|
||||
|
||||
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar
|
||||
local _, activeProf = LauncherMods.getProfiles()
|
||||
@@ -690,9 +719,12 @@ local function buildModScopeRow(imp, x, y, w, m)
|
||||
})
|
||||
|
||||
if #options >= 2 then
|
||||
for _, opt in ipairs(options) do
|
||||
local cw = Kit.textWidth("micro", opt.label) + math.floor(18 * m.s)
|
||||
if cx + cw <= profX - gap then
|
||||
local avail = profX - gap - cx
|
||||
-- Chips stay when they all fit; otherwise they used to be skipped and
|
||||
-- vanish off the portrait edge. Collapse to one menu in that case only.
|
||||
if modScopeChipsWidth(options, gap, m) <= avail then
|
||||
for _, opt in ipairs(options) do
|
||||
local cw = Kit.textWidth("micro", opt.label) + math.floor(18 * m.s)
|
||||
if Kit.chip(cx, y, cw, h, opt.label, imp.modScope == opt.id, PAL.lineStrong,
|
||||
"mod-scope-" .. tostring(opt.id or "all")) then
|
||||
local want = opt.id
|
||||
@@ -701,6 +733,15 @@ local function buildModScopeRow(imp, x, y, w, m)
|
||||
end
|
||||
cx = cx + cw + gap
|
||||
end
|
||||
elseif avail > 0 then
|
||||
local shown = Kit.ellipsize("micro", modScopeCurrentLabel(imp, options),
|
||||
math.max(0, avail - math.floor(18 * m.s)))
|
||||
local cw = math.min(avail,
|
||||
Kit.textWidth("micro", shown) + math.floor(18 * m.s))
|
||||
if Kit.chip(cx, y, cw, h, shown, true, PAL.lineStrong, "mod-scope-menu") then
|
||||
queueAction(imp, "mod-scope-menu",
|
||||
function() imp._modScopePopup = true end)
|
||||
end
|
||||
end
|
||||
end
|
||||
return h + math.floor(8 * m.s)
|
||||
@@ -740,8 +781,8 @@ local function setPage(imp, key, v)
|
||||
imp._pages[key] = v
|
||||
end
|
||||
|
||||
-- A hand-drawn X, for the same reason drawCheck exists below: the UI font has
|
||||
-- no guaranteed glyph, and the launcher ships no icon asset for it.
|
||||
-- A hand-drawn X / check: the UI font has no guaranteed glyph for either,
|
||||
-- and the launcher ships no icon asset for them.
|
||||
local function drawCross(x, y, size, color)
|
||||
love.graphics.push("all")
|
||||
love.graphics.setColor(color)
|
||||
@@ -752,6 +793,18 @@ local function drawCross(x, y, size, color)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
local function drawCheck(x, y, size, color)
|
||||
love.graphics.push("all")
|
||||
love.graphics.setColor(color)
|
||||
love.graphics.setLineWidth(math.max(2.2, size * 0.17))
|
||||
love.graphics.setLineJoin("bevel")
|
||||
love.graphics.line(
|
||||
x + size * 0.02, y + size * 0.52,
|
||||
x + size * 0.38, y + size * 0.80,
|
||||
x + size * 1.015, y + size * 0.18)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- header
|
||||
-- Rail, logo row (settings and quit on the right), tab bar.
|
||||
-- Returns the y at which content may start. Its vertical arithmetic is
|
||||
@@ -1316,20 +1369,32 @@ local function buildGamePanel(imp, x, y, w, availH, m, version)
|
||||
or tostring(version)
|
||||
local ready = (not locked) and imp.ready[version] or false
|
||||
|
||||
-- title + status tag
|
||||
-- title + status tag. Ready is a check chip (the font has no tick glyph);
|
||||
-- missing ROM stays a yellow "ROM REQUIRED" tag so it still reads as an action.
|
||||
local titleH = Kit.textHeight("title")
|
||||
Kit.text("title", Kit.ellipsize("title", gameName, w * 0.6), x, y, PAL.heading)
|
||||
local tagText, tagCol
|
||||
if ready then tagText, tagCol = Strings("GOOD TO GO"), PAL.green
|
||||
elseif imp.baseRoms and imp.baseRoms[version] then
|
||||
tagText, tagCol = Strings("ROM FOUND"), PAL.green
|
||||
elseif locked then tagText, tagCol = Strings("COMING SOON"), PAL.steel
|
||||
else tagText, tagCol = Strings("ROM REQUIRED"), PAL.yellow end
|
||||
local tagW = Kit.textWidth("micro", tagText) + math.floor(18 * m.s)
|
||||
local tagH = Kit.textHeight("micro") + math.floor(10 * m.s)
|
||||
local tagX = x + Kit.textWidth("title", Kit.ellipsize("title", gameName, w * 0.6))
|
||||
+ math.floor(12 * m.s)
|
||||
Kit.tag(tagX, y + (titleH - tagH) / 2, tagW, tagH, tagText, tagCol)
|
||||
local tagY = y + (titleH - tagH) / 2
|
||||
local tagW, tagCol
|
||||
if ready then
|
||||
tagCol = PAL.green
|
||||
tagW = tagH
|
||||
if love.graphics then
|
||||
Theme.strokeRounded(tagX, tagY, tagW, tagH, tagCol, 0.7, 1)
|
||||
local ck = math.floor(tagH * 0.55)
|
||||
drawCheck(tagX + (tagW - ck) / 2, tagY + (tagH - ck) / 2, ck, tagCol)
|
||||
end
|
||||
else
|
||||
local tagText
|
||||
if imp.baseRoms and imp.baseRoms[version] then
|
||||
tagText, tagCol = Strings("ROM FOUND"), PAL.green
|
||||
elseif locked then tagText, tagCol = Strings("COMING SOON"), PAL.steel
|
||||
else tagText, tagCol = Strings("ROM REQUIRED"), PAL.yellow end
|
||||
tagW = Kit.textWidth("micro", tagText) + math.floor(18 * m.s)
|
||||
Kit.tag(tagX, tagY, tagW, tagH, tagText, tagCol)
|
||||
end
|
||||
if ready then
|
||||
local hint = Strings("(PRESS THE CART TO PLAY)")
|
||||
local hintX = tagX + tagW + math.floor(10 * m.s)
|
||||
@@ -1337,8 +1402,11 @@ local function buildGamePanel(imp, x, y, w, availH, m, version)
|
||||
Kit.text("micro", Kit.ellipsize("micro", hint, hintW), hintX,
|
||||
y + (titleH - Kit.textHeight("micro")) / 2, PAL.heading)
|
||||
end
|
||||
local cy = y + titleH + math.floor(12 * m.s)
|
||||
local remaining = availH - (titleH + math.floor(12 * m.s))
|
||||
-- Extra gap under the title when the cart is showing: 12px left the 3D
|
||||
-- shell sitting on the hairline. Scaled, and still small on a phone.
|
||||
local afterTitle = math.floor((ready and 22 or 12) * m.s)
|
||||
local cy = y + titleH + afterTitle
|
||||
local remaining = availH - (titleH + afterTitle)
|
||||
|
||||
local gap = m.gap
|
||||
local lx, lw, rx2, rw
|
||||
@@ -1446,20 +1514,6 @@ local function currentSort(imp)
|
||||
return sortKey
|
||||
end
|
||||
|
||||
-- A hand-drawn check mark: the UI font has no guaranteed glyph for one, and
|
||||
-- a tofu box on the "you already have this" signal would be worse than none.
|
||||
local function drawCheck(x, y, size, color)
|
||||
love.graphics.push("all")
|
||||
love.graphics.setColor(color)
|
||||
love.graphics.setLineWidth(math.max(2.2, size * 0.17))
|
||||
love.graphics.setLineJoin("bevel")
|
||||
love.graphics.line(
|
||||
x + size * 0.02, y + size * 0.52,
|
||||
x + size * 0.38, y + size * 0.80,
|
||||
x + size * 1.015, y + size * 0.18)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
-- One compact coloured checkbox for each game. The cartridge colour carries
|
||||
-- the game identity even when the row is narrow.
|
||||
local function modGameCheckbox(x, y, size, checked, game, id)
|
||||
@@ -1978,21 +2032,47 @@ local TRUST_WARNING = "if you did not get this from bryanthaboi's github "
|
||||
.. "it might have been tampered with. go to the discord to verify "
|
||||
.. COMMUNITY_URL .. " (or click the logo above)"
|
||||
|
||||
-- Mark + optional updater + Patch notes. Chips match the mark's 22px
|
||||
-- height so they do not read as bigger than the logo; the row can still
|
||||
-- be tapMin tall for spacing. On a phone the notes chip drops onto a
|
||||
-- second row rather than overflowing the mark.
|
||||
local function footerLayout(imp, m, markW)
|
||||
local markH = math.floor(22 * m.s)
|
||||
local rowH = math.max(markH, Kit.tapMin())
|
||||
local notesLabel = Strings("Patch notes")
|
||||
-- Tight chip, still enough for Kit.button's labelInset so the words survive.
|
||||
local chipPad = math.floor(24 * m.s)
|
||||
local nw = Kit.textWidth("micro", notesLabel) + chipPad
|
||||
local upStatus, upLabel, upAction, upGlow = LauncherView._updateControl(imp)
|
||||
local uw = upStatus
|
||||
and (Kit.textWidth("micro", upLabel) + chipPad) or 0
|
||||
local gap = math.floor(10 * m.s)
|
||||
local inner = m.w - 2 * m.pad
|
||||
local topW = (markW or 0) + (upStatus and (gap + uw) or 0) + gap + nw
|
||||
return {
|
||||
rowH = rowH, chipH = markH, gap = gap,
|
||||
notesLabel = notesLabel, nw = nw,
|
||||
upStatus = upStatus, upLabel = upLabel, upAction = upAction, upGlow = upGlow,
|
||||
uw = uw, wrap = topW > inner,
|
||||
}
|
||||
end
|
||||
|
||||
-- Pinned to the bottom of the window; returns the y it starts at, so the
|
||||
-- panels above know how much room they have.
|
||||
-- Deliberately compact: at a large UI scale the footer is pure overhead
|
||||
-- competing with the panel for a short window's height, so the mark and the
|
||||
-- link share one line and the trust warning is capped at a single line.
|
||||
local function footerHeight(imp, m)
|
||||
-- Top pad + mark/update row + gap + the FULL wrapped trust message +
|
||||
-- bottom pad. The message wraps to as many lines as it needs: truncating
|
||||
-- a trust warning defeats its purpose, and the bottom pad is not optional
|
||||
-- either (without it the last line sits flush on the window edge and its
|
||||
-- lower half clips off). The row is tapMin tall because the small update
|
||||
-- button rides beside the mark.
|
||||
local rowH = math.max(math.floor(22 * m.s), Kit.tapMin())
|
||||
return math.floor(8 * m.s) + rowH + math.floor(6 * m.s)
|
||||
+ Kit.wrapHeight("micro", TRUST_WARNING, m.contentW)
|
||||
-- Top pad + mark/update row + optional notes wrap row + gap + the FULL
|
||||
-- wrapped trust message + bottom pad. The message wraps to as many lines
|
||||
-- as it needs: truncating a trust warning defeats its purpose, and the
|
||||
-- bottom pad is not optional either (without it the last line sits flush
|
||||
-- on the window edge and its lower half clips off). The row is tapMin
|
||||
-- tall because the small update button rides beside the mark.
|
||||
local f = footerLayout(imp, m, math.floor(130 * m.s))
|
||||
local h = math.floor(8 * m.s) + f.rowH + math.floor(6 * m.s)
|
||||
if f.wrap then h = h + f.chipH + math.floor(6 * m.s) end
|
||||
return h + Kit.wrapHeight("micro", TRUST_WARNING, m.contentW)
|
||||
+ math.floor(8 * m.s)
|
||||
end
|
||||
|
||||
@@ -2009,19 +2089,17 @@ local function buildFooter(imp, m, y)
|
||||
local bw, bh = imp.bcg:getDimensions()
|
||||
local scale = math.min((130 * m.s) / bw, (22 * m.s) / bh)
|
||||
local dw, dh = bw * scale, bh * scale
|
||||
local rowH = math.max(math.floor(22 * m.s), Kit.tapMin())
|
||||
-- The mark and the small self-update control share the row, centred as a
|
||||
-- group. The updater moved down here from the header, where it overlapped
|
||||
-- the wordmark on a phone; small on purpose, its glow still carries the
|
||||
-- "act on me" signal.
|
||||
local upStatus, upLabel, upAction, upGlow = LauncherView._updateControl(imp)
|
||||
-- Kit.button insets its label 16*scale per side, so the width must budget
|
||||
-- more than that or the label ellipsizes ("Check for updat...").
|
||||
local uw = upStatus
|
||||
and (Kit.textWidth("micro", upLabel) + math.floor(36 * m.s)) or 0
|
||||
local groupW = dw + (upStatus and (math.floor(10 * m.s) + uw) or 0)
|
||||
local bx = m.x + math.floor((m.w - groupW) / 2)
|
||||
local f = footerLayout(imp, m, dw)
|
||||
local rowH, gap, chipH = f.rowH, f.gap, f.chipH
|
||||
-- The mark, the small self-update control, and Patch notes share the row,
|
||||
-- centred as a group. The updater moved down here from the header, where
|
||||
-- it overlapped the wordmark on a phone; small on purpose, its glow still
|
||||
-- carries the "act on me" signal. Notes wrap under the mark on a phone.
|
||||
local topW = dw + (f.upStatus and (gap + f.uw) or 0)
|
||||
if not f.wrap then topW = topW + gap + f.nw end
|
||||
local bx = m.x + math.floor((m.w - topW) / 2)
|
||||
local my = cy + math.floor((rowH - dh) / 2)
|
||||
local chipY = cy + math.floor((rowH - chipH) / 2)
|
||||
local hot = Kit.hover(bx, my, dw, dh)
|
||||
love.graphics.setShader(imp.invertShader)
|
||||
love.graphics.setColor(1, 1, 1, hot and 1 or 0.85)
|
||||
@@ -2031,14 +2109,30 @@ local function buildFooter(imp, m, y)
|
||||
if Kit.press(bx, my, dw, dh) then
|
||||
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
||||
end
|
||||
if upStatus then
|
||||
btn(imp, bx + dw + math.floor(10 * m.s), cy, uw, rowH, "updater",
|
||||
upLabel, {
|
||||
kind = upGlow and "warn" or "ghost", font = "micro",
|
||||
glow = upGlow, action = upAction,
|
||||
local cx = bx + dw
|
||||
if f.upStatus then
|
||||
cx = cx + gap
|
||||
btn(imp, cx, chipY, f.uw, chipH, "updater",
|
||||
f.upLabel, {
|
||||
kind = f.upGlow and "warn" or "ghost", font = "micro",
|
||||
glow = f.upGlow, action = f.upAction,
|
||||
})
|
||||
cx = cx + f.uw
|
||||
end
|
||||
local function notesBtn(x, y)
|
||||
btn(imp, x, y, f.nw, chipH, "patch-notes", f.notesLabel, {
|
||||
kind = "ghost", font = "micro",
|
||||
action = function() imp._appPatchNotes = true end,
|
||||
})
|
||||
end
|
||||
if f.wrap then
|
||||
cy = cy + rowH + gap
|
||||
notesBtn(m.x + math.floor((m.w - f.nw) / 2), cy)
|
||||
cy = cy + chipH + math.floor(6 * m.s)
|
||||
else
|
||||
notesBtn(cx + gap, chipY)
|
||||
cy = cy + rowH + math.floor(6 * m.s)
|
||||
end
|
||||
cy = cy + rowH + math.floor(6 * m.s)
|
||||
-- The trust message wraps in full, each line centred under the mark, and
|
||||
-- the URL inside it IS the link -- no separate link floating elsewhere.
|
||||
-- font:getWrap never splits an unspaced word, so the URL stays whole on
|
||||
@@ -2543,6 +2637,34 @@ local function buildSortModal(imp, m)
|
||||
action = function() imp._sortPopup = nil end })
|
||||
end
|
||||
|
||||
-- Game-scope chooser used when the Show-for chips cannot all fit on the
|
||||
-- mods toolbar (portrait phones). Same options as the chip row.
|
||||
local function buildModScopeModal(imp, m)
|
||||
local options = modScopeOptions(imp)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(360 * m.s)
|
||||
local gap = math.floor(8 * m.s)
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
+ #options * (m.btnH + gap) + m.btnH + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local cy = py + pad
|
||||
Kit.text("button", Strings("Show for"), px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
for _, opt in ipairs(options) do
|
||||
local key = tostring(opt.id or "all")
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "scopepop-" .. key, opt.label, {
|
||||
kind = (imp.modScope == opt.id) and "primary" or "ghost", font = "small",
|
||||
action = function()
|
||||
imp:_setModScope(opt.id)
|
||||
imp._modScopePopup = nil
|
||||
end })
|
||||
cy = cy + m.btnH + gap
|
||||
end
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "scopepop-close",
|
||||
Strings("Close"), { font = "small",
|
||||
action = function() imp._modScopePopup = nil end })
|
||||
end
|
||||
|
||||
-- Category filter for FIND MODS. Two columns, because an index can list
|
||||
-- enough categories to overflow a single stacked column on a short window.
|
||||
local function buildFilterModal(imp, m)
|
||||
@@ -3253,8 +3375,9 @@ end
|
||||
local function modalUp(imp)
|
||||
return (imp._settingsText or imp._settings or imp._rename
|
||||
or imp._indexPrompt or imp._modConfirm or imp._modReleaseNotes
|
||||
or imp._appPatchNotes
|
||||
or imp._findDetails or imp._modVersions or imp._modDepResolver or imp._sortPopup
|
||||
or imp._filterPopup or imp._indexManage or imp._modActions
|
||||
or imp._filterPopup or imp._modScopePopup or imp._indexManage or imp._modActions
|
||||
or imp._modHeaderActionsPopup or imp._profilesPopup or imp._singleProfileActions or imp._profileSavePrompt
|
||||
or imp._profileRenamePrompt or imp._findEntry or imp._gameManage) ~= nil
|
||||
end
|
||||
@@ -3351,6 +3474,20 @@ local function buildModals(imp, m)
|
||||
return true
|
||||
end
|
||||
if imp._modConfirm then buildConfirmModal(imp, m) return true end
|
||||
if imp._appPatchNotes then
|
||||
local PatchNotes = require("src.update.PatchNotes")
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
local raw, ver = PatchNotes.body(imp.Check)
|
||||
local body = ModUpdate.cleanBody(raw or "", 0)
|
||||
if body == "" then body = Strings("(No patch notes.)") end
|
||||
local title = Strings("Patch notes")
|
||||
if ver and ver ~= "" then
|
||||
title = title .. " v" .. tostring(ver)
|
||||
end
|
||||
buildTextModal(imp, m, "patch-notes-modal", title, body,
|
||||
function() imp._appPatchNotes = nil end)
|
||||
return true
|
||||
end
|
||||
if imp._modReleaseNotes then
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
local n = imp._modReleaseNotes
|
||||
@@ -3380,6 +3517,7 @@ local function buildModals(imp, m)
|
||||
if imp._profilesPopup then buildProfilesModal(imp, m) return true end
|
||||
if imp._modHeaderActionsPopup then buildModHeaderActionsModal(imp, m) return true end
|
||||
if imp._sortPopup then buildSortModal(imp, m) return true end
|
||||
if imp._modScopePopup then buildModScopeModal(imp, m) return true end
|
||||
if imp._filterPopup then buildFilterModal(imp, m) return true end
|
||||
if imp._indexManage then buildIndexesModal(imp, m) return true end
|
||||
if imp._modActions then buildModActionsModal(imp, m) return true end
|
||||
|
||||
@@ -2820,7 +2820,7 @@ function RomImporter:keypressed(key)
|
||||
return
|
||||
end
|
||||
if self._modConfirm or self._modVersions or self._modReleaseNotes
|
||||
or self._findDetails then
|
||||
or self._findDetails or self._appPatchNotes then
|
||||
-- Focus navigation belongs to the visible modal as well as the launcher
|
||||
-- beneath it. Route arrows and an already-armed confirm before this guard
|
||||
-- returns; unarmed Enter still falls through to the modal guard. Keep this
|
||||
@@ -2833,6 +2833,8 @@ function RomImporter:keypressed(key)
|
||||
self._findDetails = nil
|
||||
elseif self._modReleaseNotes then
|
||||
self._modReleaseNotes = nil
|
||||
elseif self._appPatchNotes then
|
||||
self._appPatchNotes = nil
|
||||
else
|
||||
self._modConfirm = nil
|
||||
self._modVersions = nil
|
||||
|
||||
@@ -154,7 +154,7 @@ function HallOfFame.monPlacements(mon, def)
|
||||
put(out, genderGlyph(mon.gender), 18, 13)
|
||||
-- (8,14) is a bare '/', so the nickname starts at (9,14).
|
||||
put(out, "/", 8, 14)
|
||||
put(out, mon.nickname or mon.species, 9, 14)
|
||||
put(out, mon.nickname or mon.name or mon.species, 9, 14)
|
||||
put(out, levelText(mon.level), 1, 16)
|
||||
end
|
||||
-- '<ID>' '№' '/' at (7,16), (8,16), (9,16), then five digits at (10,16).
|
||||
|
||||
@@ -630,7 +630,7 @@ function PackMenu:openTeachParty(row)
|
||||
if not allowed then
|
||||
if game.say then
|
||||
game:say(("%s can't learn %s!"):format(
|
||||
mon.nickname or mon.species or "?", moveName))
|
||||
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -638,7 +638,7 @@ function PackMenu:openTeachParty(row)
|
||||
if move.id == moveId then
|
||||
if game.say then
|
||||
game:say(("%s already knows %s!"):format(
|
||||
mon.nickname or mon.species or "?", moveName))
|
||||
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
+12
-3
@@ -71,6 +71,9 @@ function Check.parseRelease(jsonText, Json)
|
||||
payloadName = payloadName,
|
||||
payload = Check.pickAsset(doc.assets, payloadName),
|
||||
sums = Check.pickAsset(doc.assets, "sha256sums.txt"),
|
||||
-- GitHub release body: already fetched with the update check, shown by
|
||||
-- the launcher's Patch notes footer button.
|
||||
notes = type(doc.body) == "string" and doc.body or "",
|
||||
}
|
||||
end
|
||||
|
||||
@@ -135,6 +138,10 @@ local function drain()
|
||||
if stateCh then
|
||||
local msg = stateCh:pop()
|
||||
while msg do
|
||||
if type(msg) == "table" and type(msg.notes) ~= "string"
|
||||
and type(cache.notes) == "string" then
|
||||
msg.notes = cache.notes
|
||||
end
|
||||
cache = msg
|
||||
msg = stateCh:pop()
|
||||
end
|
||||
@@ -158,11 +165,11 @@ function Check.start()
|
||||
return
|
||||
end
|
||||
requested = true
|
||||
cache = { status = "checking" }
|
||||
cache = { status = "checking", notes = cache.notes, latest = cache.latest }
|
||||
cmdCh:push({ cmd = "check" })
|
||||
end
|
||||
|
||||
-- Current snapshot: { status, latest, progress, error }. status is one of
|
||||
-- Current snapshot: { status, latest, progress, error, notes }. status is one of
|
||||
-- idle | checking | uptodate | available | downloading | ready | needs_full | error.
|
||||
function Check.state()
|
||||
drain()
|
||||
@@ -171,6 +178,7 @@ function Check.state()
|
||||
latest = cache.latest,
|
||||
progress = cache.progress,
|
||||
error = cache.error,
|
||||
notes = cache.notes,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -180,7 +188,8 @@ function Check.download()
|
||||
drain()
|
||||
if not cmdCh then return end
|
||||
if cache.status ~= "available" then return end
|
||||
cache = { status = "downloading", latest = cache.latest, progress = 0 }
|
||||
cache = { status = "downloading", latest = cache.latest, progress = 0,
|
||||
notes = cache.notes }
|
||||
cmdCh:push({ cmd = "download" })
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
-- Resolve the game's patch notes for the launcher footer modal.
|
||||
--
|
||||
-- Sources, first hit wins:
|
||||
-- 1. The GitHub body the self-updater already fetched (Check.parseRelease)
|
||||
-- 2. PATCH_NOTES.md, if a build packed one
|
||||
-- 3. mobile/ios/app-repo.json -- CI copies each release's notes into
|
||||
-- versions[].localizedDescription, so a checkout already has them
|
||||
-- on disk even when the updater has not run
|
||||
|
||||
local Json = require("src.link.Json")
|
||||
|
||||
local PatchNotes = {}
|
||||
|
||||
PatchNotes.FILES = {
|
||||
"PATCH_NOTES.md",
|
||||
"assets/PATCH_NOTES.md",
|
||||
}
|
||||
|
||||
PatchNotes.REPO_FILES = {
|
||||
"mobile/ios/app-repo.json",
|
||||
}
|
||||
|
||||
local function nonempty(s)
|
||||
return type(s) == "string" and s:find("%S")
|
||||
end
|
||||
|
||||
function PatchNotes.fromCheck(Check)
|
||||
if not (Check and Check.state) then return nil, nil end
|
||||
local ok, st = pcall(Check.state)
|
||||
st = (ok and type(st) == "table") and st or nil
|
||||
if not st then return nil, nil end
|
||||
if nonempty(st.notes) then
|
||||
return st.notes, st.latest
|
||||
end
|
||||
return nil, st.latest
|
||||
end
|
||||
|
||||
local function readPath(path)
|
||||
local fs = love and love.filesystem
|
||||
if fs and fs.read then
|
||||
local ok, text = pcall(fs.read, path)
|
||||
if ok and nonempty(text) then return text end
|
||||
end
|
||||
local f = io.open(path, "rb")
|
||||
if not f then return nil end
|
||||
local text = f:read("*a")
|
||||
f:close()
|
||||
return nonempty(text) and text or nil
|
||||
end
|
||||
|
||||
function PatchNotes.fromFile()
|
||||
for _, path in ipairs(PatchNotes.FILES) do
|
||||
local text = readPath(path)
|
||||
if text then return text end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Newest-first list of { version, notes } from the iOS sidecar.
|
||||
function PatchNotes.parseRepo(jsonText)
|
||||
local doc = Json.decode(jsonText)
|
||||
if type(doc) ~= "table" or type(doc.apps) ~= "table" then return {} end
|
||||
local out = {}
|
||||
for _, app in ipairs(doc.apps) do
|
||||
if type(app) == "table" and type(app.versions) == "table" then
|
||||
for _, row in ipairs(app.versions) do
|
||||
if type(row) == "table" and nonempty(row.localizedDescription) then
|
||||
out[#out + 1] = {
|
||||
version = row.version,
|
||||
notes = row.localizedDescription,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function PatchNotes.fromRepo(engine)
|
||||
for _, path in ipairs(PatchNotes.REPO_FILES) do
|
||||
local text = readPath(path)
|
||||
if text then
|
||||
local list = PatchNotes.parseRepo(text)
|
||||
if #list == 0 then return nil, nil end
|
||||
if engine and engine ~= "0.0.0-dev" then
|
||||
for _, row in ipairs(list) do
|
||||
if row.version == engine then
|
||||
return row.notes, row.version
|
||||
end
|
||||
end
|
||||
end
|
||||
return list[1].notes, list[1].version
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
function PatchNotes.body(Check)
|
||||
local notes, ver = PatchNotes.fromCheck(Check)
|
||||
if notes then return notes, ver end
|
||||
notes = PatchNotes.fromFile()
|
||||
if notes then return notes, ver end
|
||||
local Version = require("src.core.Version")
|
||||
local engine = (Version and Version.engine) or "?"
|
||||
notes, ver = PatchNotes.fromRepo(engine)
|
||||
if notes then return notes, ver end
|
||||
return "No patch notes loaded yet for gen1recomp v" .. engine .. ".\n\n"
|
||||
.. "They appear here after the launcher checks GitHub for the latest "
|
||||
.. "release.", engine
|
||||
end
|
||||
|
||||
return PatchNotes
|
||||
@@ -45,7 +45,12 @@ local Boot = loadModule("src/update/Boot.lua")
|
||||
local cmdCh = love.thread.getChannel("update_check_cmd")
|
||||
local stateCh = love.thread.getChannel("update_check_state")
|
||||
|
||||
local function post(t) stateCh:push(t) end
|
||||
local function post(t)
|
||||
if pending and type(t) == "table" and t.notes == nil then
|
||||
t.notes = pending.notes
|
||||
end
|
||||
stateCh:push(t)
|
||||
end
|
||||
|
||||
local osName = (love.system and love.system.getOS and love.system.getOS()) or ""
|
||||
local isWindows = osName == "Windows"
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
-- Bake a Gen 2 map canvas without a World instance. The save editor's map
|
||||
-- tab uses this so Gold rooms draw the same tiles the overworld would,
|
||||
-- instead of the checkerboard fallback Map2 has no renderer for.
|
||||
|
||||
local Assets = require("src.render.Assets")
|
||||
local BorderFill = require("src.world.gen2.BorderFill")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Palettes = require("src.world.gen2.Palettes")
|
||||
|
||||
local MapPreview = {}
|
||||
|
||||
local ROOF_TILESETS = {
|
||||
TILESET_JOHTO = true,
|
||||
TILESET_JOHTO_MODERN = true,
|
||||
TILESET_KANTO = true,
|
||||
}
|
||||
|
||||
local function applyRoofOverlay(atlasPath, roofPath, tilesPerRow)
|
||||
local atlasData = love.image.newImageData(Assets.resolve(atlasPath))
|
||||
local roofData = love.image.newImageData(Assets.resolve(roofPath))
|
||||
for t = 0, 8 do
|
||||
local destId = 0x0a + t
|
||||
local dx = (destId % tilesPerRow) * 8
|
||||
local dy = math.floor(destId / tilesPerRow) * 8
|
||||
local sx = t * 8
|
||||
for y = 0, 7 do
|
||||
for x = 0, 7 do
|
||||
local r, g, b, a = roofData:getPixel(sx + x, y)
|
||||
atlasData:setPixel(dx + x, dy + y, r, g, b, a)
|
||||
end
|
||||
end
|
||||
end
|
||||
local image = love.graphics.newImage(atlasData)
|
||||
image:setFilter("nearest", "nearest")
|
||||
return image
|
||||
end
|
||||
|
||||
local function optionalTable(data, gen2Key, plainKey, generated)
|
||||
if data[gen2Key] then return data[gen2Key] end
|
||||
if data[plainKey] then return data[plainKey] end
|
||||
local ok, mod = pcall(require, "data.generated." .. generated)
|
||||
if ok and type(mod) == "table" then return mod end
|
||||
return nil
|
||||
end
|
||||
|
||||
function MapPreview.baker(data)
|
||||
data = data or {}
|
||||
return {
|
||||
tilesets = data.gen2Tilesets or data.tilesets or {},
|
||||
-- Data:load does not pull roofs.lua; the Gold cache still has it.
|
||||
roofs = optionalTable(data, "gen2Roofs", "roofs", "roofs"),
|
||||
palettes = optionalTable(data, "gen2Palettes", "palettes", "palettes"),
|
||||
atlasCache = {},
|
||||
mapImages = {},
|
||||
}
|
||||
end
|
||||
|
||||
function MapPreview.atlasFor(baker, mapDef)
|
||||
if not (baker and mapDef) then return nil, nil end
|
||||
local tileset = baker.tilesets and baker.tilesets[mapDef.tileset]
|
||||
if not tileset then return nil, nil end
|
||||
local cacheKey = mapDef.tileset
|
||||
local roofName = nil
|
||||
local roofs = baker.roofs
|
||||
if ROOF_TILESETS[mapDef.tileset] then
|
||||
roofName = roofs and roofs.mapGroupRoofs and roofs.mapGroupRoofs[mapDef.group]
|
||||
end
|
||||
if roofName then cacheKey = cacheKey .. "|" .. roofName end
|
||||
local cached = baker.atlasCache[cacheKey]
|
||||
if cached then return cached, tileset end
|
||||
|
||||
local tilesPerRow = tileset.tilesPerRow or 16
|
||||
local atlas
|
||||
local roofSpec = roofName and roofs and roofs.roofs and roofs.roofs[roofName]
|
||||
if roofSpec and roofSpec.image and love.image and love.image.newImageData then
|
||||
local ok, img = pcall(applyRoofOverlay, tileset.image, roofSpec.image, tilesPerRow)
|
||||
if ok then atlas = img end
|
||||
end
|
||||
if not atlas then
|
||||
if not tileset.image then return nil, tileset end
|
||||
local ok, img = pcall(Assets.image, tileset.image)
|
||||
if not ok then return nil, tileset end
|
||||
atlas = img
|
||||
if atlas.setFilter then atlas:setFilter("nearest", "nearest") end
|
||||
end
|
||||
baker.atlasCache[cacheKey] = atlas
|
||||
return atlas, tileset
|
||||
end
|
||||
|
||||
-- Same bake as World:bakeMapImage (src/world/gen2/World.lua), minus the
|
||||
-- World fields the overworld keeps for anim overlays and cave flicker.
|
||||
function MapPreview.bake(baker, map, daytime)
|
||||
local atlas, tileset = MapPreview.atlasFor(baker, map.def)
|
||||
if not atlas or not tileset then return nil end
|
||||
if not (love.graphics and love.graphics.newCanvas) then return nil end
|
||||
local blocks = tileset.blocks
|
||||
local tilesPerRow = tileset.tilesPerRow or 16
|
||||
local pw, ph = map.width * 32, map.height * 32
|
||||
local okCanvas, canvas = pcall(love.graphics.newCanvas, pw, ph)
|
||||
if not okCanvas or not canvas then return nil end
|
||||
if canvas.setFilter then canvas:setFilter("nearest", "nearest") end
|
||||
local quads = {}
|
||||
local function quadFor(tile)
|
||||
local q = quads[tile]
|
||||
if q then return q end
|
||||
local sx = (tile % tilesPerRow) * 8
|
||||
local sy = math.floor(tile / tilesPerRow) * 8
|
||||
q = love.graphics.newQuad(sx, sy, 8, 8, atlas:getDimensions())
|
||||
quads[tile] = q
|
||||
return q
|
||||
end
|
||||
|
||||
local tilePalettes = tileset.tilePalettes
|
||||
local bgSet = baker.palettes and daytime
|
||||
and Palettes.bgSet(baker.palettes, map.def, daytime) or nil
|
||||
local colored = bgSet and tilePalettes and GbcPalette.available()
|
||||
local clearColor = { 0.15, 0.55, 0.25 }
|
||||
if bgSet and bgSet[1] and bgSet[1][1] then
|
||||
local c = GbcPalette.color(bgSet[1], 1)
|
||||
clearColor = { c[1] / 255, c[2] / 255, c[3] / 255 }
|
||||
end
|
||||
|
||||
local function drawTiles(slot)
|
||||
for by = 0, map.height - 1 do
|
||||
for bx = 0, map.width - 1 do
|
||||
local blockId = BorderFill.blockFor(
|
||||
map.blocks[by * map.width + bx + 1], map.borderBlock)
|
||||
local block = blocks and blocks[(blockId or 0) + 1]
|
||||
if block then
|
||||
for i = 0, 15 do
|
||||
local tile = block[i + 1] or 0
|
||||
local tileSlot = tilePalettes and tilePalettes[tile + 1] or 1
|
||||
if not slot or tileSlot == slot then
|
||||
local tx = bx * 32 + (i % 4) * 8
|
||||
local ty = by * 32 + math.floor(i / 4) * 8
|
||||
love.graphics.draw(atlas, quadFor(tile), tx, ty)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function paint()
|
||||
love.graphics.clear(clearColor[1], clearColor[2], clearColor[3], 1)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.push()
|
||||
love.graphics.origin()
|
||||
if colored then
|
||||
for slot = 1, 8 do
|
||||
GbcPalette.with(bgSet[slot], function() drawTiles(slot) end)
|
||||
end
|
||||
else
|
||||
drawTiles(nil)
|
||||
end
|
||||
love.graphics.pop()
|
||||
end
|
||||
if canvas.renderTo then
|
||||
canvas:renderTo(paint)
|
||||
else
|
||||
paint()
|
||||
end
|
||||
return canvas
|
||||
end
|
||||
|
||||
function MapPreview.imageFor(baker, map)
|
||||
if not (baker and map and map.id) then return nil end
|
||||
local cached = baker.mapImages[map.id]
|
||||
if cached then return cached end
|
||||
local img = MapPreview.bake(baker, map, "DAY")
|
||||
baker.mapImages[map.id] = img or false
|
||||
return img
|
||||
end
|
||||
|
||||
function MapPreview.renderer(baker, map)
|
||||
local img = MapPreview.imageFor(baker, map)
|
||||
if not img then return nil end
|
||||
return {
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(img, -math.floor(camX or 0), -math.floor(camY or 0))
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return MapPreview
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
-- Mods "Show for" chips collapse to a dropdown when they cannot all fit.
|
||||
-- Landscape / desktop keep the individual game chips. No pokered cite: the
|
||||
-- launcher is port-only chrome.
|
||||
-- luajit tests/engine/launcher_mod_scope_dropdown.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
love.graphics.setLineJoin = love.graphics.setLineJoin or function() end
|
||||
love.graphics.newShader = love.graphics.newShader or function() return {} end
|
||||
|
||||
local Kit = require("src.ui.kit.Kit")
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
local LauncherView = require("src.import.LauncherView")
|
||||
|
||||
local function window(w, h)
|
||||
love.graphics.getDimensions = function() return w, h end
|
||||
love.graphics.getPixelDimensions = function() return w, h end
|
||||
end
|
||||
|
||||
local function navIds()
|
||||
local ids = {}
|
||||
for i = 1, (Kit._navPrevN or 0) do
|
||||
local slot = Kit._nav[i]
|
||||
if slot and slot.id then ids[slot.id] = true end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
local function drawMods(W, H)
|
||||
window(W, H)
|
||||
local imp = RomImporter.new(function() end, { launcher = true })
|
||||
imp.tab = "mods"
|
||||
imp.ready = { red = true, blue = true, yellow = true, gold = true }
|
||||
local ok, err = pcall(LauncherView.draw, imp)
|
||||
check(ok, ("%dx%d mods draws: %s"):format(W, H, tostring(err)))
|
||||
return navIds()
|
||||
end
|
||||
|
||||
local portrait = drawMods(360, 780)
|
||||
check(portrait["mod-scope-menu"] == true,
|
||||
"portrait: Show-for collapses to a dropdown when the chips cannot all fit")
|
||||
check(portrait["mod-scope-gold"] == nil
|
||||
and portrait["mod-scope-red"] == nil
|
||||
and portrait["mod-scope-blue"] == nil
|
||||
and portrait["mod-scope-yellow"] == nil,
|
||||
"portrait: individual version chips are not drawn beside the dropdown")
|
||||
|
||||
local desktop = drawMods(1280, 720)
|
||||
check(desktop["mod-scope-menu"] == nil,
|
||||
"desktop: chips fit, so there is no dropdown")
|
||||
check(desktop["mod-scope-all"] == true
|
||||
and desktop["mod-scope-red"] == true
|
||||
and desktop["mod-scope-blue"] == true
|
||||
and desktop["mod-scope-yellow"] == true
|
||||
and desktop["mod-scope-gold"] == true,
|
||||
"desktop: every Show-for chip stays reachable")
|
||||
|
||||
print("ok launcher mod scope dropdown")
|
||||
@@ -23,7 +23,7 @@ local function importer(field)
|
||||
end
|
||||
|
||||
local modalFields = {
|
||||
"_modConfirm", "_modVersions", "_modReleaseNotes", "_findDetails",
|
||||
"_modConfirm", "_modVersions", "_modReleaseNotes", "_appPatchNotes", "_findDetails",
|
||||
}
|
||||
|
||||
for _, field in ipairs(modalFields) do
|
||||
@@ -77,6 +77,12 @@ do
|
||||
imp:keypressed("escape")
|
||||
eq(imp._modReleaseNotes, nil, "Escape closes release notes")
|
||||
end
|
||||
do
|
||||
resetFocus()
|
||||
local imp = importer("_appPatchNotes")
|
||||
imp:keypressed("escape")
|
||||
eq(imp._appPatchNotes, nil, "Escape closes patch notes")
|
||||
end
|
||||
do
|
||||
resetFocus()
|
||||
local imp = importer("_modVersions")
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
-- Launcher footer Patch notes control. The GitHub release body is already
|
||||
-- fetched by the updater; this is the in-app viewer for it.
|
||||
-- luajit tests/engine/launcher_patch_notes.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
love.graphics.setLineJoin = love.graphics.setLineJoin or function() end
|
||||
love.graphics.newShader = love.graphics.newShader or function() return {} end
|
||||
|
||||
local Kit = require("src.ui.kit.Kit")
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
local LauncherView = require("src.import.LauncherView")
|
||||
local PatchNotes = require("src.update.PatchNotes")
|
||||
|
||||
local function window(w, h)
|
||||
love.graphics.getDimensions = function() return w, h end
|
||||
love.graphics.getPixelDimensions = function() return w, h end
|
||||
end
|
||||
|
||||
local function freshLauncher()
|
||||
return RomImporter.new(function() end, { launcher = true })
|
||||
end
|
||||
|
||||
local realPrint = love.graphics.print
|
||||
local function drawAndCapture(imp)
|
||||
local seen = {}
|
||||
love.graphics.print = function(str, ...)
|
||||
seen[#seen + 1] = tostring(str)
|
||||
return realPrint(str, ...)
|
||||
end
|
||||
local ok, err = pcall(LauncherView.draw, imp)
|
||||
love.graphics.print = realPrint
|
||||
check(ok, "the frame draws: " .. tostring(err))
|
||||
return table.concat(seen, "\n")
|
||||
end
|
||||
|
||||
window(1280, 720)
|
||||
local imp = freshLauncher()
|
||||
local text = drawAndCapture(imp)
|
||||
check(text:find("Patch notes", 1, true) ~= nil,
|
||||
"desktop footer prints Patch notes")
|
||||
|
||||
window(360, 780)
|
||||
local phone = freshLauncher()
|
||||
check(drawAndCapture(phone):find("Patch notes", 1, true) ~= nil,
|
||||
"portrait footer still prints Patch notes")
|
||||
|
||||
do
|
||||
local body, ver = PatchNotes.body({
|
||||
state = function()
|
||||
return { notes = "## Issues closed\n\n- #12 cart padding", latest = "1.4.2" }
|
||||
end,
|
||||
})
|
||||
eq(body, "## Issues closed\n\n- #12 cart padding",
|
||||
"PatchNotes prefers the updater's GitHub body")
|
||||
eq(ver, "1.4.2", "PatchNotes carries the release version")
|
||||
end
|
||||
|
||||
do
|
||||
local body, ver = PatchNotes.body(nil)
|
||||
check(type(body) == "string" and body:find("Issues closed", 1, true),
|
||||
"without a check result PatchNotes uses the stashed iOS app-repo notes")
|
||||
check(type(ver) == "string" and ver:find("^%d+%.%d+%.%d+$") ~= nil,
|
||||
"stashed notes name a release version")
|
||||
end
|
||||
|
||||
do
|
||||
local f = assert(io.open("mobile/ios/app-repo.json", "rb"))
|
||||
local list = PatchNotes.parseRepo(f:read("*a"))
|
||||
f:close()
|
||||
check(#list >= 2, "app-repo.json stashes more than one release")
|
||||
local notes, ver = PatchNotes.fromRepo(list[2].version)
|
||||
eq(ver, list[2].version, "fromRepo can pick a specific stashed version")
|
||||
eq(notes, list[2].notes, "fromRepo returns that version's notes")
|
||||
end
|
||||
|
||||
imp._appPatchNotes = true
|
||||
local modal = drawAndCapture(imp)
|
||||
check(modal:find("Patch notes", 1, true) ~= nil, "the modal titles itself")
|
||||
check(modal:find("Close", 1, true) ~= nil, "the modal can be closed")
|
||||
|
||||
imp:keypressed("escape")
|
||||
eq(imp._appPatchNotes, nil, "Escape dismisses patch notes")
|
||||
|
||||
T.finish("launcher patch notes")
|
||||
@@ -32,6 +32,15 @@ eq(rel.payloadName, "gen1recomp-1.4.2.love", "payload name derived from version"
|
||||
eq(rel.payload.url, "http://x/love", "payload asset url picked")
|
||||
eq(rel.payload.size, 12345, "payload asset size picked")
|
||||
eq(rel.sums.url, "http://x/sums", "sums asset url picked")
|
||||
eq(rel.notes, "", "missing release body becomes empty notes")
|
||||
|
||||
local withNotes = Check.parseRelease(Json.encode({
|
||||
tag_name = "v1.4.2",
|
||||
body = "## Issues closed\n\n- #1 cart padding",
|
||||
assets = {},
|
||||
}))
|
||||
eq(withNotes.notes, "## Issues closed\n\n- #1 cart padding",
|
||||
"parseRelease keeps the GitHub release body")
|
||||
|
||||
-- a newer release that ships no .love yet: parses, but the payload/sums are nil
|
||||
-- so the worker will route to needs_full rather than an in-place update
|
||||
|
||||
@@ -287,6 +287,7 @@ eq(rel.payload.url, "http://x/love", "parseRelease picks the payload asset url")
|
||||
eq(rel.payload.size, 12345, "parseRelease picks the payload asset size")
|
||||
eq(rel.sums.url, "http://x/sums", "parseRelease picks the sums asset url")
|
||||
eq(rel.sums.size, 99, "parseRelease picks the sums asset size")
|
||||
eq(rel.notes, "", "parseRelease treats a missing body as empty notes")
|
||||
|
||||
-- a release with no .love yet still parses; payload/sums are nil so the worker
|
||||
-- routes to a full reinstall rather than an in-place update
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
-- tests/save_editor_task7_tests.lua
|
||||
-- tests/save_editor_task8_tests.lua
|
||||
-- tests/save_editor_mod_tests.lua
|
||||
-- tests/save_editor_gen2_tests.lua
|
||||
-- See tools/save-editor/README.md for the full list.
|
||||
--
|
||||
-- All of them drive tools/save-editor/Ops.lua rather than clicking pixel
|
||||
|
||||
@@ -3475,6 +3475,8 @@ do
|
||||
local lua = (arg and arg[-1]) or "luajit"
|
||||
local status = os.execute(("%q tests/save_editor_mod_tests.lua"):format(lua))
|
||||
check(status == 0 or status == true, "save_editor_mod_tests suite")
|
||||
status = os.execute(("%q tests/save_editor_gen2_tests.lua"):format(lua))
|
||||
check(status == 0 or status == true, "save_editor_gen2_tests suite")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------- input hold regressions
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
-- Headless Gold save-editor rules. Run from repo root:
|
||||
-- luajit tests/save_editor_gen2_tests.lua
|
||||
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua"
|
||||
.. ";./tools/save-editor/panels/?.lua"
|
||||
|
||||
local love_stub = require("tests.love_stub")
|
||||
love = love_stub
|
||||
|
||||
local passed, failed = 0, 0
|
||||
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL: " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function eq(a, b, msg)
|
||||
check(a == b, msg .. string.format(" (got %s, want %s)", tostring(a), tostring(b)))
|
||||
end
|
||||
|
||||
print("== save editor gen2 tests ==")
|
||||
|
||||
local Gen = require("Gen")
|
||||
local Catalog = require("Catalog")
|
||||
local MonOps = require("MonOps")
|
||||
local Ops = require("Ops")
|
||||
local State = require("State")
|
||||
local Save2 = require("src.core.gen2.Save")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
local data = {
|
||||
pokemon = {
|
||||
CYNDAQUIL = {
|
||||
id = "CYNDAQUIL", name = "CYNDAQUIL", dex = 155,
|
||||
types = { "FIRE" },
|
||||
baseStats = {
|
||||
hp = 39, attack = 52, defense = 43, speed = 65,
|
||||
specialAttack = 60, specialDefense = 50,
|
||||
},
|
||||
catchRate = 45, baseExp = 65,
|
||||
growthRate = "MEDIUM_FAST",
|
||||
levelMoves = { { level = 1, move = "TACKLE" } },
|
||||
genderRatio = 31,
|
||||
},
|
||||
TOTODILE = {
|
||||
id = "TOTODILE", name = "TOTODILE", dex = 158,
|
||||
types = { "WATER" },
|
||||
baseStats = {
|
||||
hp = 50, attack = 65, defense = 64, speed = 43,
|
||||
specialAttack = 44, specialDefense = 48,
|
||||
},
|
||||
catchRate = 45, baseExp = 66,
|
||||
growthRate = "MEDIUM_FAST",
|
||||
levelMoves = { { level = 1, move = "SCRATCH" } },
|
||||
genderRatio = 31,
|
||||
},
|
||||
},
|
||||
moves = {
|
||||
TACKLE = { pp = 35 },
|
||||
SCRATCH = { pp = 35 },
|
||||
},
|
||||
items = {
|
||||
POTION = { pocket = "ITEM" },
|
||||
MASTER_BALL = { pocket = "BALL" },
|
||||
FLOWER_MAIL = { pocket = "ITEM" },
|
||||
},
|
||||
maps = {},
|
||||
}
|
||||
|
||||
local function newState()
|
||||
local S = State.new()
|
||||
S.data = data
|
||||
S.cat = Catalog.build(data)
|
||||
S.save = Save2.newGame()
|
||||
S.version = "gold"
|
||||
Gen.ensureBoxes(S.save)
|
||||
return S
|
||||
end
|
||||
|
||||
do
|
||||
GameVersion.set("gold")
|
||||
eq(Gen.of({ generation = 2 }), 2, "Gen.of generation field")
|
||||
eq(Gen.of({ version = "gold" }), 2, "Gen.of version gold")
|
||||
eq(Gen.of(SaveData.newGame()), 1, "Gen.of gen1 newGame")
|
||||
eq(Gen.of(Save2.newGame()), 2, "Gen.of gold newGame")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
check(Ops.speciesUsable(S, "CYNDAQUIL"), "spa/spd species is usable")
|
||||
local S1 = State.new()
|
||||
S1.data = {
|
||||
pokemon = {
|
||||
PIDGEY = { baseStats = { hp = 40, attack = 45, defense = 40, speed = 56, special = 35 } },
|
||||
BROKEN = { baseStats = { hp = 1 } },
|
||||
},
|
||||
}
|
||||
check(Ops.speciesUsable(S1, "PIDGEY"), "gen1 special species is usable")
|
||||
check(not Ops.speciesUsable(S1, "BROKEN"), "partial record is not usable")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.partyAdd(S)
|
||||
eq(#S.save.party, 1, "partyAdd on gold")
|
||||
local mon = S.save.party[1]
|
||||
eq(mon.species, "CYNDAQUIL", "first catalog species")
|
||||
check(mon.experience ~= nil, "gold mon has experience")
|
||||
check(mon.exp == nil or mon.experience ~= nil, "does not rely on gen1 exp")
|
||||
check(mon.stats.specialAttack and mon.stats.specialDefense,
|
||||
"gold stats have spa/spd")
|
||||
check(mon.happiness ~= nil, "gold mon has happiness")
|
||||
eq(mon.ot, S.save.player.name, "stampOT copies player name")
|
||||
|
||||
Ops.setLevel(S, mon, 20)
|
||||
eq(mon.level, 20, "setLevel 20")
|
||||
check(mon.experience > 0, "experience resynced")
|
||||
|
||||
Ops.setHappiness(S, mon, 200)
|
||||
eq(mon.happiness, 200, "happiness 200")
|
||||
Ops.setPokerus(S, mon, 15)
|
||||
eq(mon.pokerus, 15, "pokerus byte")
|
||||
Ops.setHeldItem(S, mon, "POTION")
|
||||
eq(mon.item, "POTION", "held item")
|
||||
|
||||
eq(mon.name, "CYNDAQUIL", "new mon copies species display name")
|
||||
Ops.setSpecies(S, mon, "TOTODILE")
|
||||
eq(mon.species, "TOTODILE", "setSpecies id")
|
||||
eq(mon.name, "TOTODILE", "setSpecies rewrites the Gold display name")
|
||||
check(mon.nickname == nil, "setSpecies does not invent a nickname")
|
||||
eq(mon.types[1], "WATER", "setSpecies rewrites copied types")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.partyAdd(S)
|
||||
Ops.partyAdd(S)
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
Mail.set(S.save, 1, Mail.entry("FLOWER_MAIL", "hi", "GOLD", 1, "CYNDAQUIL"))
|
||||
Mail.set(S.save, 2, Mail.entry("SURF_MAIL", "bye", "GOLD", 1, "CYNDAQUIL"))
|
||||
S.selectedParty = 1
|
||||
Ops.partyMove(S, 1)
|
||||
eq(Mail.state(S.save).party[1].message, "bye", "partyMove carries mail with the mon")
|
||||
eq(Mail.state(S.save).party[2].message, "hi", "partyMove swaps the other letter")
|
||||
S.selectedParty = 1
|
||||
check(Ops.partyRemove(S) == false, "partyRemove arms")
|
||||
check(Ops.partyRemove(S) == true, "partyRemove commits")
|
||||
eq(Mail.state(S.save).party[1].message, "hi", "partyRemove shifts leftover mail up")
|
||||
check(Mail.state(S.save).party[2] == nil, "partyRemove clears the vacated slot")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.partyAdd(S)
|
||||
local mon = S.save.party[1]
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
Ops.setHeldItem(S, mon, "FLOWER_MAIL")
|
||||
eq(mon.item, "FLOWER_MAIL", "held mail item")
|
||||
local letter = Mail.state(S.save).party[1]
|
||||
check(letter ~= nil, "giving mail writes sPartyMail")
|
||||
eq(letter.species, "CYNDAQUIL", "new letter stamps current species")
|
||||
Ops.setSpecies(S, mon, "TOTODILE")
|
||||
eq(Mail.state(S.save).party[1].species, "TOTODILE",
|
||||
"setSpecies updates the letter's species copy")
|
||||
Ops.setHeldItem(S, mon, "POTION")
|
||||
check(Mail.state(S.save).party[1] == nil, "non-mail held item drops the letter")
|
||||
end
|
||||
|
||||
do
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local S = newState()
|
||||
local stale = Mon.new(data, "CYNDAQUIL", 5)
|
||||
stale.species = "TOTODILE"
|
||||
stale.name = "CYNDAQUIL"
|
||||
stale.types = { "FIRE" }
|
||||
S.save.dayCare = { man = { mon = stale }, lady = {} }
|
||||
Mon.syncSaveIdentity(S.save, data)
|
||||
eq(stale.name, "TOTODILE", "syncSaveIdentity rewrites Day-Care display name")
|
||||
eq(stale.types[1], "WATER", "syncSaveIdentity rewrites Day-Care types")
|
||||
eq(Mon.displayName({ nickname = nil, name = "ABRA", species = "RAYQUAZA" }),
|
||||
"ABRA", "displayName prefers the species copy over the id")
|
||||
eq(Mon.displayName({ nickname = "BOB", name = "ABRA", species = "RAYQUAZA" }),
|
||||
"BOB", "displayName prefers nickname")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
eq(Ops.boxCount(S), 14, "14 gold boxes")
|
||||
eq(Ops.boxCapacity(S), 20, "20 per box")
|
||||
Ops.boxAdd(S)
|
||||
eq(#Ops.boxes(S)[1], 1, "boxAdd into box 1")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.partyAdd(S)
|
||||
S.save.party[1].hp = 0
|
||||
Ops.partyAdd(S)
|
||||
S.selectedParty = 1
|
||||
S.selectedBox = 1
|
||||
local ok = Ops.deposit(S)
|
||||
check(ok, "deposit fainted mon while a healthy remains")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.partyAdd(S)
|
||||
Ops.partyAdd(S)
|
||||
S.selectedParty = 1
|
||||
S.selectedBox = 1
|
||||
local ok = Ops.deposit(S)
|
||||
check(ok, "deposit one of two healthy mons")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.partyAdd(S)
|
||||
S.selectedParty = 1
|
||||
S.selectedBox = 1
|
||||
local ok = Ops.deposit(S)
|
||||
check(not ok, "refuse depositing last healthy mon")
|
||||
check(S.status:lower():find("last", 1, true) or S.status:find("POKéMON")
|
||||
or S.status:find("POKEMON") or S.status:find("last"),
|
||||
"deposit refusal names the last-healthy rule: " .. tostring(S.status))
|
||||
eq(#S.save.party, 1, "party still has the mon")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
eq(Gen.money(S.save), 3000, "gold start money on player")
|
||||
Ops.addMoney(S, 1000)
|
||||
eq(S.save.player.money, 4000, "money writes player.money")
|
||||
check(S.save.money == nil or S.save.money ~= 4000, "does not write save.money")
|
||||
Ops.maxMoney(S)
|
||||
eq(S.save.player.money, 999999, "money cap")
|
||||
Ops.addCoins(S, 250)
|
||||
eq(S.save.player.coins, 250, "coins write player.coins")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
check(not Gen.hasBadge(S.save, "ZEPHYR"), "no zephyr yet")
|
||||
Ops.toggleBadge(S, "ZEPHYR")
|
||||
check(Gen.hasBadge(S.save, "ZEPHYR"), "zephyr earned")
|
||||
check(S.save.player.badges.ZEPHYR, "stored on player.badges")
|
||||
Ops.toggleBadge(S, "BOULDER")
|
||||
check(S.save.player.kantoBadges.BOULDER, "kanto badge store")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
Ops.dexOwned(S, "CYNDAQUIL", true)
|
||||
check(S.save.pokedex.caught.CYNDAQUIL, "dex writes caught")
|
||||
check(S.save.pokedex.owned == nil or S.save.pokedex.owned.CYNDAQUIL == nil,
|
||||
"does not write owned on gold")
|
||||
check(S.save.pokedex.seen.CYNDAQUIL, "owned implies seen")
|
||||
local _, owned = Ops.dexCounts(S)
|
||||
eq(owned, 1, "dexCounts reads caught")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
local name = "EVENT_BEAT_FALKNER"
|
||||
Ops.setFlag(S, name, true)
|
||||
check(Gen.getFlag(S.save, name), "gold EVENT_ sets bitfield")
|
||||
check(S.save.flags[name] == nil, "numeric flags are not string keys")
|
||||
Ops.setFlag(S, "MOD_EDITMON_GIFT", true)
|
||||
check(S.save.flags.MOD_EDITMON_GIFT, "mod flags stay named on gold")
|
||||
end
|
||||
|
||||
do
|
||||
local S = newState()
|
||||
S.mapId = "NEW_BARK_TOWN"
|
||||
S.mapClickCell = { cx = 4, cy = 5 }
|
||||
Ops.setPlayerHere(S)
|
||||
eq(S.save.position.map, "NEW_BARK_TOWN", "position.map")
|
||||
eq(S.save.position.x, 4, "position.x")
|
||||
eq(S.save.position.y, 5, "position.y")
|
||||
check(S.save.player.map == nil or S.save.player.map ~= "NEW_BARK_TOWN",
|
||||
"does not write player.map on gold")
|
||||
end
|
||||
|
||||
do
|
||||
local encoded = SaveData.encode(Save2.newGame())
|
||||
local back = SaveData.decode(encoded)
|
||||
eq(back.generation, 2, "round-trip keeps generation 2")
|
||||
end
|
||||
|
||||
do
|
||||
local names = Catalog.goldEventList()
|
||||
local hasFalkner = false
|
||||
for _, n in ipairs(names) do
|
||||
if n == "EVENT_BEAT_FALKNER" then hasFalkner = true break end
|
||||
end
|
||||
check(hasFalkner, "gold event list includes EVENT_BEAT_FALKNER")
|
||||
end
|
||||
|
||||
do
|
||||
local maps = Gen.maps({ gen2Maps = { AZALEA_GYM = true }, maps = { PALLET_TOWN = true } })
|
||||
check(maps.AZALEA_GYM, "Gen.maps includes gen2Maps")
|
||||
check(maps.PALLET_TOWN, "Gen.maps keeps Data:load maps beside gen2Maps")
|
||||
check(Gen.maps({ maps = { PALLET_TOWN = true } }).PALLET_TOWN,
|
||||
"Gen.maps falls back to maps")
|
||||
local mansion = Gen.maps({
|
||||
maps = {
|
||||
CELADON_MANSION_2F = { id = "CELADON_MANSION_2F", width = 4, height = 5 },
|
||||
},
|
||||
gen2Maps = {
|
||||
CELADON_MANSION_2F = { objects = { { name = "NPC" } } },
|
||||
BERRY_FARM = { id = "BERRY_FARM", width = 19, height = 12 },
|
||||
},
|
||||
})
|
||||
eq(mansion.CELADON_MANSION_2F.width, 4,
|
||||
"Gen.maps keeps extractor width under a gen2Maps objects patch")
|
||||
eq(mansion.CELADON_MANSION_2F.objects[1].name, "NPC",
|
||||
"Gen.maps still applies the gen2Maps patch fields")
|
||||
eq(mansion.BERRY_FARM.width, 19, "Gen.maps keeps mod maps only on gen2Maps")
|
||||
local bound = Gen.bindGoldData({ maps = { A = true }, tilesets = { T = true } })
|
||||
check(bound.gen2Maps == bound.maps, "bindGoldData aliases gen2Maps")
|
||||
check(bound.gen2Tilesets == bound.tilesets, "bindGoldData aliases gen2Tilesets")
|
||||
check(Gen.tilesets({ gen2Tilesets = { TILESET_GYM = true } }).TILESET_GYM,
|
||||
"Gen.tilesets prefers gen2Tilesets")
|
||||
end
|
||||
|
||||
do
|
||||
local Map2 = require("src.world.gen2.Map")
|
||||
local MapPreview = require("src.world.gen2.MapPreview")
|
||||
local def = {
|
||||
id = "AZALEA_GYM", tileset = "TILESET_GYM",
|
||||
width = 1, height = 1, blocks = { 1 }, borderBlock = 1,
|
||||
warps = {}, environment = "INDOOR",
|
||||
}
|
||||
local tileset = {
|
||||
id = "TILESET_GYM",
|
||||
image = "assets/generated/tilesets/gym.png",
|
||||
tilesPerRow = 16,
|
||||
blocks = { { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
|
||||
}
|
||||
local map = Map2.new(def, tileset)
|
||||
check(map.renderer == nil, "Map2 does not ship a renderer")
|
||||
local baker = MapPreview.baker({ tilesets = { TILESET_GYM = tileset } })
|
||||
local renderer = MapPreview.renderer(baker, map)
|
||||
check(renderer ~= nil and renderer.draw ~= nil,
|
||||
"MapPreview attaches a draw for a Gold map")
|
||||
end
|
||||
|
||||
print(string.format("save editor gen2 tests: %d passed, %d failed", passed, failed))
|
||||
if failed > 0 then os.exit(1) end
|
||||
@@ -142,6 +142,41 @@ local ok, err = pcall(function()
|
||||
local report = SaveData.validate(probe, Data)
|
||||
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
||||
"validate keeps the modded mon while the mod is enabled")
|
||||
|
||||
-- Gold-targeted mods: spa/spd records are usable, and a Gen 1-only
|
||||
-- manifest stays out of Gold (no ROM cache / App.load("gold") required).
|
||||
local ModTargets = require("src.mods.ModTargets")
|
||||
local Ops = require("Ops")
|
||||
check(not ModTargets.supports({}, "gold"),
|
||||
"legacy gen1-only fixture does not support gold")
|
||||
check(not ModTargets.supports({ games = { "red" } }, "gold"),
|
||||
"explicit gen1 games list does not support gold")
|
||||
check(ModTargets.supports({ games = { "gold" } }, "gold"),
|
||||
"gold-targeted manifest supports gold")
|
||||
check(ModTargets.supports({ gen2compat = true }, "gold"),
|
||||
"gen2compat legacy still supports gold")
|
||||
|
||||
local goldS = {
|
||||
data = {
|
||||
pokemon = {
|
||||
EDITMON = {
|
||||
baseStats = {
|
||||
hp = 50, attack = 50, defense = 50, speed = 50,
|
||||
specialAttack = 50, specialDefense = 50,
|
||||
},
|
||||
},
|
||||
G1ONLY = {
|
||||
baseStats = {
|
||||
hp = 50, attack = 50, defense = 50, speed = 50, special = 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
check(Ops.speciesUsable(goldS, "EDITMON"),
|
||||
"spa/spd EDITMON is usable on gold")
|
||||
check(Ops.speciesUsable(goldS, "G1ONLY"),
|
||||
"gen1 special record remains usable (dual-key gate)")
|
||||
end)
|
||||
|
||||
os.remove(MOD_ROOT .. "/main.lua")
|
||||
@@ -154,7 +189,7 @@ os.remove(tmpPath)
|
||||
love.filesystem = savedFS
|
||||
-- leave shared singletons the way we found them (the fixture merged one
|
||||
-- record into Data.pokemon)
|
||||
Data.pokemon.EDITMON = nil
|
||||
if Data.pokemon then Data.pokemon.EDITMON = nil end
|
||||
Assets.loader = savedBridge
|
||||
Assets.invalidate()
|
||||
Runtime.install(savedEvents, savedHooks, savedErrors)
|
||||
|
||||
@@ -12,7 +12,7 @@ package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local Flags = dofile("tools/goldwalk/flags.lua")
|
||||
|
||||
local ROOT = arg[1] or "../pokegold"
|
||||
local OUT = "tests/drivers/gold/flag_names.lua"
|
||||
local OUT = "src/core/gen2/FlagNames.lua"
|
||||
|
||||
local events, _ = Flags.parse(ROOT .. "/constants/event_flags.asm")
|
||||
local engine, _ = Flags.parse(ROOT .. "/constants/engine_flags.asm")
|
||||
|
||||
+49
-42
@@ -25,6 +25,7 @@ local State = require("State")
|
||||
local Kit = require("Kit")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local PadInput = require("PadInput")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
@@ -87,56 +88,43 @@ local function applyLoaded(path, statusVerb)
|
||||
if save then
|
||||
S.save = save
|
||||
S.status = statusVerb .. " " .. path
|
||||
S.mapId = save.player.map
|
||||
S.loadError = false
|
||||
S.allowSave = true
|
||||
elseif existed then
|
||||
-- File is present but SaveIO.load couldn't decode it: treat it as a
|
||||
-- real (corrupt) save, not a missing one. Editing a stub here is fine,
|
||||
-- but Save must stay disabled so we never clobber the corrupt file
|
||||
-- until the user fixes it and Reload succeeds.
|
||||
S.save = require("src.core.SaveData").newGame()
|
||||
S.save = Gen.newGame(S.version)
|
||||
S.status = "Corrupt save at " .. path .. " (" .. tostring(err) ..
|
||||
"), Save disabled, use Reload after fixing the file"
|
||||
S.mapId = S.save.player.map
|
||||
S.loadError = true
|
||||
S.allowSave = false
|
||||
else
|
||||
S.save = require("src.core.SaveData").newGame()
|
||||
S.save = Gen.newGame(S.version)
|
||||
S.status = "No save at " .. path .. " (" .. tostring(err) ..
|
||||
"), editing new game stub"
|
||||
S.mapId = S.save.player.map
|
||||
S.loadError = false
|
||||
S.allowSave = true
|
||||
end
|
||||
local mapId = Gen.playerMap(S.save)
|
||||
S.mapId = mapId
|
||||
S.dirty = false
|
||||
S._quitArmed = false
|
||||
S._openArmed = false
|
||||
S.editingMon = nil
|
||||
Ops.disarm(S)
|
||||
local boxes = require("src.pokemon.Boxes").ensure(S.save)
|
||||
-- Imported .sav box mons have no stat block (box_struct stops before
|
||||
-- MON_STATS). The game derives them in SaveData.validate; the editor
|
||||
-- only validates a copy, so hydrate here for Boxes/Party/MonEditor.
|
||||
local Stats = require("src.pokemon.Stats")
|
||||
local function ensureStats(mon)
|
||||
Stats.ensure(Data.pokemon and Data.pokemon[mon.species], mon)
|
||||
end
|
||||
for _, mon in ipairs(S.save.party or {}) do ensureStats(mon) end
|
||||
for _, box in ipairs(boxes) do
|
||||
for _, mon in ipairs(box) do ensureStats(mon) end
|
||||
end
|
||||
if S.save.daycare and S.save.daycare.mon then
|
||||
ensureStats(S.save.daycare.mon)
|
||||
end
|
||||
-- what the running game would quarantine, computed on a copy so the
|
||||
-- editor never mutates the file behind the user's back
|
||||
local SaveData = require("src.core.SaveData")
|
||||
Gen.ensureBoxes(S.save)
|
||||
Gen.hydrateSave(Data, S.save)
|
||||
local probe = require("src.mods.Merge").deepCopy(S.save)
|
||||
S.validation = SaveData.validate(probe, Data)
|
||||
if not SaveData.emptyReport(S.validation) then
|
||||
S.status = S.status .. string.format(", game would quarantine: %d mons, %d items, %d maps",
|
||||
#S.validation.lostMons, #S.validation.lostItems, #S.validation.remappedMaps)
|
||||
S.validation = Gen.validate(probe, Data)
|
||||
if not Gen.emptyReport(S.save, S.validation) then
|
||||
if Gen.of(S.save, S.version) == 2 then
|
||||
S.status = S.status .. string.format(
|
||||
", game would quarantine: %d script bytes, %d mail, %d events",
|
||||
#(S.validation.lostScriptMem or {}),
|
||||
#(S.validation.lostMail or {}),
|
||||
#(S.validation.lostEvents or {}))
|
||||
else
|
||||
S.status = S.status .. string.format(", game would quarantine: %d mons, %d items, %d maps",
|
||||
#S.validation.lostMons, #S.validation.lostItems, #S.validation.remappedMaps)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,6 +140,9 @@ function App.load(pathOverride, opts)
|
||||
S.slotId = opts.slotId
|
||||
S.embedded = opts.embedded or false
|
||||
S.onClose = opts.onClose
|
||||
if opts.version then
|
||||
require("src.core.GameVersion").set(opts.version)
|
||||
end
|
||||
-- the same mod set the game loads, merged into Data before the catalogs
|
||||
-- build, so modded species/items/moves are editable and MonOps stops
|
||||
-- asserting on them
|
||||
@@ -163,6 +154,10 @@ function App.load(pathOverride, opts)
|
||||
-- loaded at least once, so it doubles as the "needs evicting" marker.
|
||||
if Data._pristineKeys then Data:unloadGenerated() end
|
||||
Data:load()
|
||||
if Gen.of(nil, opts.version) == 2
|
||||
or require("src.core.GameVersion").generation() == 2 then
|
||||
Gen.bindGoldData(Data)
|
||||
end
|
||||
local ModLoader = require("src.mods.Loader")
|
||||
mods = ModLoader.new()
|
||||
mods:load(Data)
|
||||
@@ -174,9 +169,16 @@ function App.load(pathOverride, opts)
|
||||
for _, mod in ipairs(S.mods:status().loaded) do
|
||||
modRoots[#modRoots + 1] = mod.path
|
||||
end
|
||||
S.events = Catalog.scrapeEvents("data/scripts", "data/generated/trainer_headers.lua",
|
||||
nil, modRoots)
|
||||
if Gen.of(nil, opts.version) == 2 or require("src.core.GameVersion").generation() == 2 then
|
||||
S.events = Catalog.goldEventList(modRoots)
|
||||
else
|
||||
S.events = Catalog.scrapeEvents("data/scripts", "data/generated/trainer_headers.lua",
|
||||
nil, modRoots)
|
||||
end
|
||||
applyLoaded(pathOverride or SaveIO.defaultPath(), "Loaded")
|
||||
if Gen.of(S.save, S.version) == 2 then
|
||||
S.events = Catalog.goldEventList(modRoots)
|
||||
end
|
||||
end
|
||||
|
||||
-- Switch to another save file (Open button, drag-drop, or --save arg).
|
||||
@@ -582,11 +584,9 @@ local function tabCount(id)
|
||||
return tostring(n)
|
||||
elseif id == "items" then
|
||||
local Bag = require("src.inventory.Bag")
|
||||
return ("%d/%d"):format(Bag.slots(S.save), Bag.capacity(S.data))
|
||||
return ("%d/%d"):format(Bag.slots(S.save, S.data), Bag.capacity(S.data))
|
||||
elseif id == "events" then
|
||||
local n = 0
|
||||
for _ in pairs(S.save.flags or {}) do n = n + 1 end
|
||||
return tostring(n)
|
||||
return tostring(Gen.flagCount(S.save))
|
||||
elseif id == "map" then
|
||||
-- map ids run long (REDS_HOUSE_2F); the rail is a summary, not a label
|
||||
return Kit.ellipsize("tiny", S.mapId or "", 110 * Kit.scale)
|
||||
@@ -602,21 +602,28 @@ end
|
||||
-- something. Returns what to draw plus the tab that owns the first problem,
|
||||
-- so the rail can reserve the pill's width before laying out the tiles.
|
||||
local function validationPill()
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local report = S.validation
|
||||
if not report or SaveData.emptyReport(report) then
|
||||
if not report or Gen.emptyReport(S.save, report) then
|
||||
return "Save validates clean", PAL.green, nil, true
|
||||
end
|
||||
local parts = {}
|
||||
local target
|
||||
local function add(n, singular, plural, tab)
|
||||
n = n or 0
|
||||
if n <= 0 then return end
|
||||
parts[#parts + 1] = ("%d %s"):format(n, n == 1 and singular or plural)
|
||||
target = target or tab
|
||||
end
|
||||
add(#report.lostMons, "mon", "mons", "party")
|
||||
add(#report.lostItems, "item", "items", "items")
|
||||
add(#report.remappedMaps, "map", "maps", "map")
|
||||
if Gen.of(S.save, S.version) == 2 then
|
||||
add(#(report.lostScriptMem or {}), "script byte", "script bytes", "events")
|
||||
add(#(report.lostMail or {}), "mail", "mail", "party")
|
||||
add(#(report.lostEvents or {}), "event", "events", "events")
|
||||
add(#(report.lostMapScenes or {}), "map scene", "map scenes", "map")
|
||||
else
|
||||
add(#(report.lostMons or {}), "mon", "mons", "party")
|
||||
add(#(report.lostItems or {}), "item", "items", "items")
|
||||
add(#(report.remappedMaps or {}), "map", "maps", "map")
|
||||
end
|
||||
return "Would quarantine " .. table.concat(parts, ", "), PAL.yellow, target, false
|
||||
end
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
|
||||
end
|
||||
end
|
||||
|
||||
local dirs = { scriptDir }
|
||||
local dirs = {}
|
||||
if scriptDir then dirs[#dirs + 1] = scriptDir end
|
||||
for _, dir in ipairs(extraDirs or {}) do
|
||||
dirs[#dirs + 1] = dir
|
||||
end
|
||||
@@ -110,4 +111,25 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
|
||||
return sortedKeys(found)
|
||||
end
|
||||
|
||||
function Catalog.goldEventList(extraDirs)
|
||||
local names = {}
|
||||
local ok, flags = pcall(require, "src.core.gen2.FlagNames")
|
||||
if ok and flags and flags.events then
|
||||
for name in pairs(flags.events) do
|
||||
names[#names + 1] = name
|
||||
end
|
||||
end
|
||||
table.sort(names)
|
||||
local modFlags = Catalog.scrapeEvents(nil, nil, nil, extraDirs)
|
||||
local seen = {}
|
||||
for _, name in ipairs(names) do seen[name] = true end
|
||||
for _, name in ipairs(modFlags) do
|
||||
if not seen[name] then
|
||||
names[#names + 1] = name
|
||||
seen[name] = true
|
||||
end
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
return Catalog
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
-- Generation adapter for the save editor. Panels stay generation-blind;
|
||||
-- Ops and App read Gold vs RBY through this module so a Gold write never
|
||||
-- lands in Gen 1 fields (save.money, pokedex.owned, 12 boxes, ...).
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
local Gen = {}
|
||||
|
||||
local function versionGeneration(version)
|
||||
if type(version) ~= "string" then return nil end
|
||||
local info = GameVersion.info(version)
|
||||
if info then return info.generation or 1 end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Gen.of(save, version)
|
||||
if type(save) == "table" then
|
||||
if save.generation == 2 then return 2 end
|
||||
local fromVersion = versionGeneration(save.version)
|
||||
if fromVersion then return fromVersion end
|
||||
end
|
||||
local fromArg = versionGeneration(version)
|
||||
if fromArg then return fromArg end
|
||||
return GameVersion.generation()
|
||||
end
|
||||
|
||||
function Gen.ofState(S)
|
||||
if not S then return GameVersion.generation() end
|
||||
return Gen.of(S.save, S.version)
|
||||
end
|
||||
|
||||
function Gen.is2(save, version)
|
||||
return Gen.of(save, version) == 2
|
||||
end
|
||||
|
||||
-- Data:load writes Gold maps/tilesets to the Gen 1 keys; Game2 and the mod
|
||||
-- merge write gen2Maps / gen2Tilesets. Overlay the gen2 table on the loaded
|
||||
-- cache so a mod patch that landed on an empty gen2Maps (objects only, no
|
||||
-- width) does not hide the extractor's record, and a new map like BERRY_FARM
|
||||
-- still appears.
|
||||
local function overlayRecords(base, overlay)
|
||||
if not overlay then return base or {} end
|
||||
if not base or base == overlay then return overlay end
|
||||
local out = {}
|
||||
for id, def in pairs(base) do out[id] = def end
|
||||
for id, def in pairs(overlay) do
|
||||
local prior = out[id]
|
||||
if type(def) == "table" and type(prior) == "table" then
|
||||
local merged = {}
|
||||
for k, v in pairs(prior) do merged[k] = v end
|
||||
for k, v in pairs(def) do merged[k] = v end
|
||||
out[id] = merged
|
||||
else
|
||||
out[id] = def
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function Gen.maps(data)
|
||||
if type(data) ~= "table" then return {} end
|
||||
return overlayRecords(data.maps, data.gen2Maps)
|
||||
end
|
||||
|
||||
function Gen.tilesets(data)
|
||||
if type(data) ~= "table" then return {} end
|
||||
return overlayRecords(data.tilesets, data.gen2Tilesets)
|
||||
end
|
||||
|
||||
-- Point the Gen 2 Data keys at the tables Data:load already filled, before
|
||||
-- mods:load folds into gen2Maps. Same wiring Game2 does when it boots Gold.
|
||||
function Gen.bindGoldData(data)
|
||||
if type(data) ~= "table" then return data end
|
||||
if data.maps and data.gen2Maps == nil then data.gen2Maps = data.maps end
|
||||
if data.tilesets and data.gen2Tilesets == nil then
|
||||
data.gen2Tilesets = data.tilesets
|
||||
end
|
||||
if data.palettes and data.gen2Palettes == nil then
|
||||
data.gen2Palettes = data.palettes
|
||||
end
|
||||
if data.gen2Roofs == nil and data.roofs == nil then
|
||||
local ok, roofs = pcall(require, "data.generated.roofs")
|
||||
if ok and type(roofs) == "table" then
|
||||
data.roofs = roofs
|
||||
data.gen2Roofs = roofs
|
||||
end
|
||||
elseif data.roofs and data.gen2Roofs == nil then
|
||||
data.gen2Roofs = data.roofs
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
function Gen.newGame(version)
|
||||
if (versionGeneration(version) or GameVersion.generation(version)) == 2 then
|
||||
return require("src.core.gen2.Save").newGame()
|
||||
end
|
||||
return require("src.core.SaveData").newGame()
|
||||
end
|
||||
|
||||
function Gen.validate(save, data)
|
||||
if Gen.of(save) == 2 then
|
||||
return require("src.core.gen2.Save").validate(save)
|
||||
end
|
||||
return require("src.core.SaveData").validate(save, data)
|
||||
end
|
||||
|
||||
function Gen.emptyReport(save, report)
|
||||
if Gen.of(save) == 2 then
|
||||
return require("src.core.gen2.Save").emptyReport(report)
|
||||
end
|
||||
return require("src.core.SaveData").emptyReport(report)
|
||||
end
|
||||
|
||||
function Gen.hydrateMon(data, mon)
|
||||
if type(mon) ~= "table" then return mon end
|
||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||
local gen2 = (mon.stats and mon.stats.specialAttack)
|
||||
or (def and def.baseStats and def.baseStats.specialAttack)
|
||||
or mon.experience ~= nil
|
||||
if gen2 then
|
||||
require("src.battle.gen2.Mon").refreshStats(mon, data)
|
||||
else
|
||||
require("src.pokemon.Stats").ensure(def, mon)
|
||||
end
|
||||
return mon
|
||||
end
|
||||
|
||||
-- Party, boxes, Day-Care. Gold's dayCare.man/lady/egg are not save.daycare.
|
||||
function Gen.hydrateSave(data, save)
|
||||
if type(save) ~= "table" then return save end
|
||||
if Gen.of(save) == 2 then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
Mon.eachSaveMon(save, function(mon) Mon.refreshStats(mon, data) end)
|
||||
return save
|
||||
end
|
||||
for _, mon in ipairs(save.party or {}) do Gen.hydrateMon(data, mon) end
|
||||
for _, box in ipairs(save.boxes or {}) do
|
||||
if type(box) == "table" then
|
||||
for _, mon in ipairs(box) do Gen.hydrateMon(data, mon) end
|
||||
end
|
||||
end
|
||||
if save.daycare and save.daycare.mon then
|
||||
Gen.hydrateMon(data, save.daycare.mon)
|
||||
end
|
||||
return save
|
||||
end
|
||||
|
||||
function Gen.ensureBoxes(save)
|
||||
if Gen.of(save) == 2 then
|
||||
local Boxes2 = require("src.core.gen2.Boxes")
|
||||
save.boxes = save.boxes or {}
|
||||
for i = 1, Boxes2.NUM_BOXES do
|
||||
save.boxes[i] = save.boxes[i] or {}
|
||||
end
|
||||
save.currentBox = math.max(1, math.min(Boxes2.NUM_BOXES, save.currentBox or 1))
|
||||
return save.boxes
|
||||
end
|
||||
return require("src.pokemon.Boxes").ensure(save)
|
||||
end
|
||||
|
||||
function Gen.boxCount(save)
|
||||
if Gen.of(save) == 2 then
|
||||
return require("src.core.gen2.Boxes").NUM_BOXES
|
||||
end
|
||||
return require("src.pokemon.Boxes").COUNT
|
||||
end
|
||||
|
||||
function Gen.boxCapacity(save)
|
||||
if Gen.of(save) == 2 then
|
||||
return require("src.core.gen2.Boxes").MONS_PER_BOX
|
||||
end
|
||||
return require("src.pokemon.Boxes").CAPACITY
|
||||
end
|
||||
|
||||
function Gen.money(save)
|
||||
if Gen.of(save) == 2 then
|
||||
return (save.player and save.player.money) or 0
|
||||
end
|
||||
return save.money or 0
|
||||
end
|
||||
|
||||
function Gen.setMoney(save, amount)
|
||||
if Gen.of(save) == 2 then
|
||||
save.player = save.player or {}
|
||||
save.player.money = amount
|
||||
else
|
||||
save.money = amount
|
||||
end
|
||||
end
|
||||
|
||||
function Gen.coins(save)
|
||||
if Gen.of(save) == 2 then
|
||||
return (save.player and save.player.coins) or 0
|
||||
end
|
||||
return save.coins or 0
|
||||
end
|
||||
|
||||
function Gen.setCoins(save, amount)
|
||||
if Gen.of(save) == 2 then
|
||||
save.player = save.player or {}
|
||||
save.player.coins = amount
|
||||
else
|
||||
save.coins = amount
|
||||
end
|
||||
end
|
||||
|
||||
function Gen.dexOwnedKey(save)
|
||||
if Gen.of(save) == 2 then return "caught" end
|
||||
return "owned"
|
||||
end
|
||||
|
||||
function Gen.playerMap(save)
|
||||
if Gen.of(save) == 2 then
|
||||
local p = save.position
|
||||
if p and p.map then return p.map, p.x or 0, p.y or 0, p.facing end
|
||||
return save.spawn, 0, 0
|
||||
end
|
||||
local p = save.player or {}
|
||||
return p.map, p.x or 0, p.y or 0
|
||||
end
|
||||
|
||||
function Gen.setPlayerHere(save, mapId, x, y, facing)
|
||||
if Gen.of(save) == 2 then
|
||||
local prev = save.position or {}
|
||||
save.position = {
|
||||
map = mapId,
|
||||
x = x,
|
||||
y = y,
|
||||
facing = facing or prev.facing or "down",
|
||||
}
|
||||
return
|
||||
end
|
||||
save.player = save.player or {}
|
||||
save.player.map = mapId
|
||||
save.player.x = x
|
||||
save.player.y = y
|
||||
end
|
||||
|
||||
local JOHTO = {
|
||||
"ZEPHYR", "HIVE", "PLAIN", "FOG", "MINERAL", "STORM", "GLACIER", "RISING",
|
||||
}
|
||||
local KANTO = {
|
||||
"BOULDER", "CASCADE", "THUNDER", "RAINBOW",
|
||||
"SOUL", "MARSH", "VOLCANO", "EARTH",
|
||||
}
|
||||
|
||||
function Gen.badgeIds(save, cat)
|
||||
if Gen.of(save) == 2 then
|
||||
local ids = {}
|
||||
for _, name in ipairs(JOHTO) do ids[#ids + 1] = name end
|
||||
for _, name in ipairs(KANTO) do ids[#ids + 1] = name end
|
||||
return ids
|
||||
end
|
||||
local ids = {}
|
||||
for _, id in ipairs((cat and cat.items) or {}) do
|
||||
if tostring(id):find("BADGE", 1, true) then ids[#ids + 1] = id end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
local KANTO_SET = {}
|
||||
for _, name in ipairs(KANTO) do KANTO_SET[name] = true end
|
||||
|
||||
function Gen.hasBadge(save, id)
|
||||
if Gen.of(save) == 2 then
|
||||
local p = save.player or {}
|
||||
local store = KANTO_SET[id] and (p.kantoBadges or {}) or (p.badges or {})
|
||||
if store[id] then return true end
|
||||
local list = KANTO_SET[id] and KANTO or JOHTO
|
||||
for index, name in ipairs(list) do
|
||||
if name == id then return store[index] == true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
return save.inventory and save.inventory[id] and true or false
|
||||
end
|
||||
|
||||
function Gen.toggleBadge(save, id)
|
||||
if Gen.of(save) == 2 then
|
||||
save.player = save.player or {}
|
||||
local storeName = KANTO_SET[id] and "kantoBadges" or "badges"
|
||||
save.player[storeName] = save.player[storeName] or {}
|
||||
local store = save.player[storeName]
|
||||
local on = Gen.hasBadge(save, id)
|
||||
store[id] = (not on) and true or nil
|
||||
for index, name in ipairs(KANTO_SET[id] and KANTO or JOHTO) do
|
||||
if name == id then store[index] = nil end
|
||||
end
|
||||
return not on
|
||||
end
|
||||
local on = save.inventory[id] and true or false
|
||||
save.inventory[id] = (not on) and 1 or nil
|
||||
return not on
|
||||
end
|
||||
|
||||
local function goldFlagId(name)
|
||||
local flags = require("src.core.gen2.FlagNames")
|
||||
return flags.events and flags.events[name]
|
||||
end
|
||||
|
||||
function Gen.getFlag(save, name)
|
||||
if Gen.of(save) == 2 then
|
||||
local id = goldFlagId(name)
|
||||
if id then
|
||||
local Events2 = require("src.world.gen2.Events")
|
||||
local ev = Events2.new()
|
||||
ev:restore(save.events)
|
||||
return ev:get(id)
|
||||
end
|
||||
return save.flags and save.flags[name] == true
|
||||
end
|
||||
return save.flags and save.flags[name] == true
|
||||
end
|
||||
|
||||
function Gen.setFlag(save, name, on)
|
||||
if Gen.of(save) == 2 then
|
||||
local id = goldFlagId(name)
|
||||
if id then
|
||||
local Events2 = require("src.world.gen2.Events")
|
||||
local ev = Events2.new()
|
||||
ev:restore(save.events)
|
||||
ev:set(id, on and true or false)
|
||||
save.events = ev:serialize()
|
||||
return
|
||||
end
|
||||
save.flags = save.flags or {}
|
||||
save.flags[name] = on and true or nil
|
||||
return
|
||||
end
|
||||
save.flags = save.flags or {}
|
||||
save.flags[name] = on and true or nil
|
||||
end
|
||||
|
||||
function Gen.flagCount(save)
|
||||
if Gen.of(save) == 2 then
|
||||
local n = 0
|
||||
for _ in pairs(save.events or {}) do n = n + 1 end
|
||||
for _ in pairs(save.flags or {}) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
local n = 0
|
||||
for _ in pairs(save.flags or {}) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
function Gen.exp(mon)
|
||||
return mon.experience or mon.exp or 0
|
||||
end
|
||||
|
||||
return Gen
|
||||
@@ -4,23 +4,40 @@ local Growth = require("src.pokemon.Growth")
|
||||
|
||||
local MonOps = {}
|
||||
|
||||
function MonOps.create(data, species, level)
|
||||
function MonOps.create(data, species, level, gen)
|
||||
if gen == 2 then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local mon = Mon.new(data, species, level)
|
||||
assert(mon, "unknown species " .. tostring(species))
|
||||
return mon
|
||||
end
|
||||
return Pokemon.new(data, species, level)
|
||||
end
|
||||
|
||||
function MonOps.recalc(data, mon)
|
||||
function MonOps.recalc(data, mon, gen)
|
||||
if gen == 2 or (mon.stats and mon.stats.specialAttack) or mon.experience then
|
||||
require("src.battle.gen2.Mon").refreshStats(mon, data)
|
||||
return
|
||||
end
|
||||
local def = data.pokemon[mon.species]
|
||||
assert(def, "unknown species")
|
||||
mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp)
|
||||
mon.hp = math.max(0, math.min(mon.hp or mon.stats.hp, mon.stats.hp))
|
||||
end
|
||||
|
||||
function MonOps.setLevel(data, mon, level)
|
||||
function MonOps.setLevel(data, mon, level, gen)
|
||||
level = math.max(1, math.min(100, math.floor(level)))
|
||||
local def = data.pokemon[mon.species]
|
||||
mon.level = level
|
||||
if gen == 2 or mon.experience ~= nil then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local growth = Mon.growthFor(data, def.growthRate)
|
||||
mon.experience = Mon.experienceForLevel(growth, level)
|
||||
Mon.refreshStats(mon, data)
|
||||
return
|
||||
end
|
||||
mon.exp = Growth.expForLevel(def.growthRate, level)
|
||||
MonOps.recalc(data, mon)
|
||||
MonOps.recalc(data, mon, gen)
|
||||
end
|
||||
|
||||
function MonOps.setMove(data, mon, slot, moveId)
|
||||
@@ -32,6 +49,7 @@ function MonOps.setMove(data, mon, slot, moveId)
|
||||
id = moveId,
|
||||
pp = mdef.pp + ((mon.moves[slot] and mon.moves[slot].ppUps) or 0) * math.floor(mdef.pp / 5),
|
||||
ppUps = mon.moves[slot] and mon.moves[slot].ppUps or nil,
|
||||
maxPp = mdef.pp,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -42,19 +60,38 @@ function MonOps.syncHpDv(dvs)
|
||||
return dvs
|
||||
end
|
||||
|
||||
function MonOps.setDv(data, mon, key, value)
|
||||
function MonOps.setDv(data, mon, key, value, gen)
|
||||
mon.dvs[key] = math.max(0, math.min(15, math.floor(value)))
|
||||
if key ~= "hp" then
|
||||
MonOps.syncHpDv(mon.dvs)
|
||||
end
|
||||
MonOps.recalc(data, mon)
|
||||
if gen == 2 or (mon.stats and mon.stats.specialAttack) then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
mon.dvs.hp = Mon.hpDV(mon.dvs)
|
||||
local def = data.pokemon[mon.species]
|
||||
if def then
|
||||
mon.gender = Mon.gender(def, mon.dvs, { species = mon.species, level = mon.level })
|
||||
mon.shiny = Mon.isShiny(mon.dvs, { species = mon.species, def = def, level = mon.level })
|
||||
local Unown = require("src.core.gen2.Unown")
|
||||
if mon.species == Unown.SPECIES then
|
||||
mon.unownLetter = Unown.letterFromDVs(mon.dvs)
|
||||
end
|
||||
end
|
||||
end
|
||||
MonOps.recalc(data, mon, gen)
|
||||
end
|
||||
|
||||
-- Keep level; resync exp to the species growth curve (species changes).
|
||||
function MonOps.setSpecies(data, mon, species)
|
||||
-- Gold also copies def.name onto mon.name: the party list and SUMMARY print
|
||||
-- that field when nickname is nil, so leaving the previous species' name
|
||||
-- made a swapped mon still read as ABRA (or whoever was added first).
|
||||
function MonOps.setSpecies(data, mon, species, gen)
|
||||
assert(data.pokemon[species], "unknown species")
|
||||
mon.species = species
|
||||
MonOps.setLevel(data, mon, mon.level)
|
||||
MonOps.setLevel(data, mon, mon.level, gen)
|
||||
if gen == 2 or mon.experience ~= nil or (mon.stats and mon.stats.specialAttack) then
|
||||
require("src.battle.gen2.Mon").syncIdentity(mon, data)
|
||||
end
|
||||
end
|
||||
|
||||
return MonOps
|
||||
|
||||
+246
-70
@@ -17,6 +17,7 @@ local BoxesMod = require("src.pokemon.Boxes")
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local MonOps = require("MonOps")
|
||||
local Charmap = require("src.save_convert.data.charmap")
|
||||
local Gen = require("Gen")
|
||||
|
||||
local Ops = {}
|
||||
|
||||
@@ -35,6 +36,62 @@ local function clamp(n, lo, hi)
|
||||
end
|
||||
Ops.clamp = clamp
|
||||
|
||||
local function stampNewMon(S, mon)
|
||||
if Gen.ofState(S) == 2 then
|
||||
require("src.battle.gen2.Mon").stampOT(S.save, mon)
|
||||
else
|
||||
mon.ot = S.save.player.name
|
||||
mon.otId = S.save.player.id
|
||||
end
|
||||
return mon
|
||||
end
|
||||
|
||||
local function createMon(S, species, level)
|
||||
local mon = MonOps.create(S.data, species, level, Gen.ofState(S))
|
||||
return stampNewMon(S, mon)
|
||||
end
|
||||
|
||||
local function partySlot(S, mon)
|
||||
for i, member in ipairs(S.save.party or {}) do
|
||||
if member == mon then return i end
|
||||
end
|
||||
end
|
||||
|
||||
-- Portrait mail (and CheckPokeMail) store species on the letter, not the mon.
|
||||
local function partyMailEntry(S, slot)
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
return Mail.state(S.save).party[slot]
|
||||
end
|
||||
|
||||
local function syncPartyMailSpecies(S, mon)
|
||||
if Gen.ofState(S) ~= 2 then return end
|
||||
local slot = partySlot(S, mon)
|
||||
if not slot then return end
|
||||
local entry = partyMailEntry(S, slot)
|
||||
if entry then entry.species = mon.species end
|
||||
end
|
||||
|
||||
local function syncPartyMailHeldItem(S, mon, prevItem, newItem)
|
||||
if Gen.ofState(S) ~= 2 then return end
|
||||
local slot = partySlot(S, mon)
|
||||
if not slot then return end
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
if Mail.isMail(newItem) then
|
||||
local prev = partyMailEntry(S, slot)
|
||||
local player = S.save.player or {}
|
||||
Mail.set(S.save, slot, Mail.entry(
|
||||
newItem,
|
||||
prev and prev.message or "",
|
||||
tostring(mon.otName or mon.ot or player.name or ""):sub(1, Mail.AUTHOR_LENGTH),
|
||||
mon.otId or player.id or 0,
|
||||
mon.species))
|
||||
return
|
||||
end
|
||||
if Mail.isMail(prevItem) or partyMailEntry(S, slot) then
|
||||
Mail.clear(S.save, slot)
|
||||
end
|
||||
end
|
||||
|
||||
local function now()
|
||||
if love and love.timer and love.timer.getTime then
|
||||
return love.timer.getTime()
|
||||
@@ -111,9 +168,7 @@ function Ops.partyAdd(S)
|
||||
return Ops.say(S, ("Party is full (%d/%d)"):format(#S.save.party, PartyMod.MAX))
|
||||
end
|
||||
local species = S.cat.species[1]
|
||||
local mon = MonOps.create(S.data, species, 5)
|
||||
mon.ot = S.save.player.name
|
||||
mon.otId = S.save.player.id
|
||||
local mon = createMon(S, species, 5)
|
||||
table.insert(S.save.party, mon)
|
||||
S.selectedParty = #S.save.party
|
||||
S.editingMon = mon
|
||||
@@ -129,6 +184,11 @@ function Ops.partyRemove(S)
|
||||
return false
|
||||
end
|
||||
table.remove(S.save.party, index)
|
||||
-- sPartyMail is keyed by party slot, not by mon: dropping a member without
|
||||
-- shifting letters hands the next mon someone else's mail.
|
||||
if Gen.ofState(S) == 2 then
|
||||
require("src.core.gen2.Mail").removeSlot(S.save, index)
|
||||
end
|
||||
if S.editingMon == mon then S.editingMon = nil end
|
||||
S.selectedParty = clamp(index, 1, math.max(#S.save.party, 1))
|
||||
S.editingMon = S.save.party[S.selectedParty]
|
||||
@@ -144,6 +204,9 @@ function Ops.partyMove(S, delta)
|
||||
return Ops.say(S, delta < 0 and "Already the lead mon" or "Already the last mon")
|
||||
end
|
||||
party[i], party[j] = party[j], party[i]
|
||||
if Gen.ofState(S) == 2 then
|
||||
require("src.core.gen2.Mail").swapSlots(S.save, i, j)
|
||||
end
|
||||
S.selectedParty = j
|
||||
return Ops.mark(S, ("Moved %s to slot %d"):format(party[j].species, j))
|
||||
end
|
||||
@@ -157,7 +220,7 @@ function Ops.setLevel(S, mon, level)
|
||||
if want == mon.level then
|
||||
return Ops.say(S, want == 1 and "Level is already 1" or "Level is already 100")
|
||||
end
|
||||
MonOps.setLevel(S.data, mon, want)
|
||||
MonOps.setLevel(S.data, mon, want, Gen.ofState(S))
|
||||
return Ops.mark(S, ("%s is now Lv%d"):format(mon.species, mon.level))
|
||||
end
|
||||
|
||||
@@ -172,15 +235,23 @@ end
|
||||
-- instead of trusting the list: without this, picking such a species walked
|
||||
-- Stats.calc into `speciesDef.baseStats[key]` on a nil and took the window
|
||||
-- down (#541).
|
||||
local BASE_STAT_KEYS = { "hp", "attack", "defense", "speed", "special" }
|
||||
local BASE_STAT_KEYS_G1 = { "hp", "attack", "defense", "speed", "special" }
|
||||
local BASE_STAT_KEYS_G2 = {
|
||||
"hp", "attack", "defense", "speed", "specialAttack", "specialDefense",
|
||||
}
|
||||
|
||||
local function baseStatsComplete(bs, keys)
|
||||
for _, key in ipairs(keys) do
|
||||
if type(bs[key]) ~= "number" then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Ops.speciesUsable(S, id)
|
||||
local def = id and S.data.pokemon[id]
|
||||
if type(def) ~= "table" or type(def.baseStats) ~= "table" then return false end
|
||||
for _, key in ipairs(BASE_STAT_KEYS) do
|
||||
if type(def.baseStats[key]) ~= "number" then return false end
|
||||
end
|
||||
return true
|
||||
return baseStatsComplete(def.baseStats, BASE_STAT_KEYS_G1)
|
||||
or baseStatsComplete(def.baseStats, BASE_STAT_KEYS_G2)
|
||||
end
|
||||
|
||||
-- The one funnel every species change goes through (the picker, the stepper,
|
||||
@@ -199,14 +270,19 @@ function Ops.setSpecies(S, mon, id)
|
||||
end
|
||||
-- MonOps.recalc replaces mon.stats with a fresh table rather than editing
|
||||
-- it in place, so holding the old reference is a real rollback.
|
||||
local wasSpecies, wasLevel, wasExp = mon.species, mon.level, mon.exp
|
||||
local wasStats, wasHp = mon.stats, mon.hp
|
||||
local ok, err = pcall(MonOps.setSpecies, S.data, mon, id)
|
||||
local wasSpecies, wasLevel, wasExp, wasExperience = mon.species, mon.level, mon.exp, mon.experience
|
||||
local wasStats, wasHp, wasName = mon.stats, mon.hp, mon.name
|
||||
local wasTypes, wasGender, wasShiny, wasUnown, wasMaxHp =
|
||||
mon.types, mon.gender, mon.shiny, mon.unownLetter, mon.maxHp
|
||||
local ok, err = pcall(MonOps.setSpecies, S.data, mon, id, Gen.ofState(S))
|
||||
if not ok then
|
||||
mon.species, mon.level, mon.exp = wasSpecies, wasLevel, wasExp
|
||||
mon.stats, mon.hp = wasStats, wasHp
|
||||
mon.species, mon.level, mon.exp, mon.experience = wasSpecies, wasLevel, wasExp, wasExperience
|
||||
mon.stats, mon.hp, mon.name = wasStats, wasHp, wasName
|
||||
mon.types, mon.gender, mon.shiny, mon.unownLetter, mon.maxHp =
|
||||
wasTypes, wasGender, wasShiny, wasUnown, wasMaxHp
|
||||
return Ops.say(S, ("Could not set %s: %s"):format(tostring(id), tostring(err)))
|
||||
end
|
||||
syncPartyMailSpecies(S, mon)
|
||||
return Ops.mark(S, ("Species set to %s"):format(id))
|
||||
end
|
||||
|
||||
@@ -313,9 +389,9 @@ end
|
||||
-- the target is the box, not a mon.
|
||||
function Ops.openBoxAddPicker(S, Kit)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= BoxesMod.CAPACITY then
|
||||
if #box >= Ops.boxCapacity(S) then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
||||
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||
end
|
||||
S.speciesPicker = { query = "", offset = 0, opened = true, mode = "box-add" }
|
||||
if Kit then Kit.focus = "species-picker" end -- soft keyboard rises (#529)
|
||||
@@ -327,17 +403,15 @@ end
|
||||
-- box mon and a party mon born in the editor are indistinguishable.
|
||||
function Ops.boxAddSpecies(S, id)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= BoxesMod.CAPACITY then
|
||||
if #box >= Ops.boxCapacity(S) then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
||||
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||
end
|
||||
if not Ops.speciesUsable(S, id) then
|
||||
return Ops.say(S, ("%s has no usable base stats, cannot add it")
|
||||
:format(tostring(id)))
|
||||
end
|
||||
local mon = MonOps.create(S.data, id, 5)
|
||||
mon.ot = S.save.player.name
|
||||
mon.otId = S.save.player.id
|
||||
local mon = createMon(S, id, 5)
|
||||
table.insert(box, mon)
|
||||
S.selectedBoxSlot = #box
|
||||
S.editingMon = mon
|
||||
@@ -351,7 +425,7 @@ function Ops.setDv(S, mon, key, value)
|
||||
if want == mon.dvs[key] then
|
||||
return Ops.say(S, ("%s DV is already %d"):format(key, want))
|
||||
end
|
||||
MonOps.setDv(S.data, mon, key, want)
|
||||
MonOps.setDv(S.data, mon, key, want, Gen.ofState(S))
|
||||
return Ops.mark(S, ("%s DV %d (HP DV now %d)"):format(key, mon.dvs[key], mon.dvs.hp))
|
||||
end
|
||||
|
||||
@@ -382,7 +456,17 @@ end
|
||||
function Ops.resetMoves(S, mon)
|
||||
if not mon then return false end
|
||||
local def = S.data.pokemon[mon.species]
|
||||
local learned = Pokemon.movesAtLevel(def, mon.level)
|
||||
local gen = Gen.ofState(S)
|
||||
local learned
|
||||
if gen == 2 then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
learned = {}
|
||||
for _, mv in ipairs(Mon.movesAtLevel(def, mon.level, S.data.moves)) do
|
||||
learned[#learned + 1] = mv.id
|
||||
end
|
||||
else
|
||||
learned = Pokemon.movesAtLevel(def, mon.level)
|
||||
end
|
||||
mon.moves = {}
|
||||
for slot, id in ipairs(learned) do
|
||||
MonOps.setMove(S.data, mon, slot, id)
|
||||
@@ -397,6 +481,7 @@ function Ops.healMon(S, mon)
|
||||
return Ops.say(S, ("%s is already at full HP"):format(mon.species))
|
||||
end
|
||||
mon.hp = mon.stats.hp
|
||||
if mon.maxHp then mon.maxHp = mon.stats.hp end
|
||||
mon.status = nil
|
||||
for _, mv in ipairs(mon.moves or {}) do
|
||||
local def = S.data.moves[mv.id]
|
||||
@@ -554,27 +639,35 @@ function Ops.clearNickname(S, mon)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ boxes
|
||||
function Ops.boxCount(S)
|
||||
return Gen.boxCount(S.save)
|
||||
end
|
||||
|
||||
function Ops.boxCapacity(S)
|
||||
return Gen.boxCapacity(S.save)
|
||||
end
|
||||
|
||||
function Ops.boxes(S)
|
||||
return BoxesMod.ensure(S.save)
|
||||
return Gen.ensureBoxes(S.save)
|
||||
end
|
||||
|
||||
function Ops.selectBox(S, index)
|
||||
S.selectedBox = clamp(index, 1, BoxesMod.COUNT)
|
||||
S.selectedBox = clamp(index, 1, Ops.boxCount(S))
|
||||
S.selectedBoxSlot = 1
|
||||
S.save.currentBox = S.selectedBox
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
S.status = ("Box %d (%d/%d)"):format(S.selectedBox, #box, BoxesMod.CAPACITY)
|
||||
S.status = ("Box %d (%d/%d)"):format(S.selectedBox, #box, Ops.boxCapacity(S))
|
||||
return true
|
||||
end
|
||||
|
||||
function Ops.stepBox(S, delta)
|
||||
local n = BoxesMod.COUNT
|
||||
local n = Ops.boxCount(S)
|
||||
return Ops.selectBox(S, ((S.selectedBox - 1 + delta) % n) + 1)
|
||||
end
|
||||
|
||||
function Ops.selectBoxSlot(S, index)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
S.selectedBoxSlot = clamp(index, 1, BoxesMod.CAPACITY)
|
||||
S.selectedBoxSlot = clamp(index, 1, Ops.boxCapacity(S))
|
||||
local mon = box[S.selectedBoxSlot]
|
||||
S.editingMon = mon
|
||||
S.status = mon
|
||||
@@ -589,14 +682,12 @@ end
|
||||
-- chooses what lands in the box instead of always getting catalog entry #1.
|
||||
function Ops.boxAdd(S)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= BoxesMod.CAPACITY then
|
||||
if #box >= Ops.boxCapacity(S) then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
||||
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||
end
|
||||
local species = S.cat.species[1]
|
||||
local mon = MonOps.create(S.data, species, 5)
|
||||
mon.ot = S.save.player.name
|
||||
mon.otId = S.save.player.id
|
||||
local mon = createMon(S, species, 5)
|
||||
table.insert(box, mon)
|
||||
S.selectedBoxSlot = #box
|
||||
S.editingMon = mon
|
||||
@@ -612,9 +703,16 @@ function Ops.withdraw(S)
|
||||
return Ops.say(S, ("Party is full (%d/%d), deposit one first")
|
||||
:format(#S.save.party, PartyMod.MAX))
|
||||
end
|
||||
table.remove(box, S.selectedBoxSlot)
|
||||
table.insert(S.save.party, mon)
|
||||
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#box, 1))
|
||||
if Gen.ofState(S) == 2 then
|
||||
local Boxes2 = require("src.core.gen2.Boxes")
|
||||
local ok, reason = Boxes2.canWithdraw(S.save, S.selectedBox, S.selectedBoxSlot)
|
||||
if not ok then return Ops.say(S, reason) end
|
||||
Boxes2.withdraw(S.save, S.selectedBox, S.selectedBoxSlot)
|
||||
else
|
||||
table.remove(box, S.selectedBoxSlot)
|
||||
table.insert(S.save.party, mon)
|
||||
end
|
||||
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#Ops.boxes(S)[S.selectedBox], 1))
|
||||
S.selectedParty = #S.save.party
|
||||
return Ops.mark(S, ("Withdrew %s to party slot %d"):format(mon.species, #S.save.party))
|
||||
end
|
||||
@@ -639,6 +737,17 @@ function Ops.deposit(S)
|
||||
local i = S.selectedParty
|
||||
local mon = S.save.party[i]
|
||||
if not mon then return Ops.say(S, "No party slot selected") end
|
||||
if Gen.ofState(S) == 2 then
|
||||
local Boxes2 = require("src.core.gen2.Boxes")
|
||||
local boxIndex = S.selectedBox or S.save.currentBox or 1
|
||||
local ok, reason = Boxes2.canDeposit(S.save, i, boxIndex)
|
||||
if not ok then return Ops.say(S, reason) end
|
||||
Boxes2.deposit(S.save, i, boxIndex)
|
||||
S.selectedParty = clamp(i, 1, math.max(#S.save.party, 1))
|
||||
S.selectedBox = boxIndex
|
||||
if S.editingMon == mon then S.editingMon = nil end
|
||||
return Ops.mark(S, ("Deposited %s into box %d"):format(mon.species, boxIndex))
|
||||
end
|
||||
local boxNum = BoxesMod.deposit(S.save, mon)
|
||||
if not boxNum then
|
||||
return Ops.say(S, "Every box is full, release something first")
|
||||
@@ -652,12 +761,13 @@ end
|
||||
|
||||
-- ------------------------------------------------------------------ items
|
||||
function Ops.addMoney(S, delta)
|
||||
local want = clamp((S.save.money or 0) + delta, 0, Ops.MONEY_MAX)
|
||||
if want == S.save.money then
|
||||
local have = Gen.money(S.save)
|
||||
local want = clamp(have + delta, 0, Ops.MONEY_MAX)
|
||||
if want == have then
|
||||
return Ops.say(S, delta < 0 and "Money is already $0"
|
||||
or ("Money is already capped at $%d"):format(Ops.MONEY_MAX))
|
||||
end
|
||||
S.save.money = want
|
||||
Gen.setMoney(S.save, want)
|
||||
return Ops.mark(S, ("Money set to $%d"):format(want))
|
||||
end
|
||||
|
||||
@@ -665,15 +775,29 @@ function Ops.maxMoney(S)
|
||||
return Ops.addMoney(S, Ops.MONEY_MAX)
|
||||
end
|
||||
|
||||
Ops.COIN_MAX = 9999
|
||||
|
||||
function Ops.addCoins(S, delta)
|
||||
local have = Gen.coins(S.save)
|
||||
local want = clamp(have + delta, 0, Ops.COIN_MAX)
|
||||
if want == have then
|
||||
return Ops.say(S, delta < 0 and "Coins are already 0"
|
||||
or ("Coins are already capped at %d"):format(Ops.COIN_MAX))
|
||||
end
|
||||
Gen.setCoins(S.save, want)
|
||||
return Ops.mark(S, ("Coins set to %d"):format(want))
|
||||
end
|
||||
|
||||
function Ops.addToBag(S, id)
|
||||
if not id then return Ops.say(S, "Pick an item first") end
|
||||
local capacity = Bag.capacity(S.data)
|
||||
local pocket = Bag.pocketOf(id, S.data)
|
||||
local capacity = Bag.capacity(S.data, pocket)
|
||||
if Bag.add(S.save, id, 1, S.data) then
|
||||
return Ops.mark(S, ("Added %s to the bag (%d/%d slots)")
|
||||
:format(id, Bag.slots(S.save), capacity))
|
||||
return Ops.mark(S, ("Added %s to the bag (%d/%d %s slots)")
|
||||
:format(id, Bag.slots(S.save, S.data, pocket), capacity, pocket))
|
||||
end
|
||||
return Ops.say(S, ("Bag is full (%d/%d slots)")
|
||||
:format(Bag.slots(S.save), capacity))
|
||||
return Ops.say(S, ("Bag is full (%d/%d %s slots)")
|
||||
:format(Bag.slots(S.save, S.data, pocket), capacity, pocket))
|
||||
end
|
||||
|
||||
function Ops.bagAdjust(S, id, delta)
|
||||
@@ -715,6 +839,12 @@ end
|
||||
function Ops.addToPc(S, id)
|
||||
if not id then return Ops.say(S, "Pick an item first") end
|
||||
local pc = Ops.pcItems(S)
|
||||
local n = 0
|
||||
for _ in pairs(pc) do n = n + 1 end
|
||||
if not pc[id] and Gen.ofState(S) == 2 and n >= 50 then
|
||||
return Ops.say(S, "PC item storage is full (50 stacks)")
|
||||
end
|
||||
local pc = Ops.pcItems(S)
|
||||
pc[id] = math.min(Ops.STACK_MAX, (pc[id] or 0) + 1)
|
||||
return Ops.mark(S, ("%s x%d in PC storage"):format(id, pc[id]))
|
||||
end
|
||||
@@ -749,27 +879,17 @@ function Ops.isBadgeId(id)
|
||||
end
|
||||
|
||||
function Ops.badgeIds(S)
|
||||
local ids = {}
|
||||
for _, id in ipairs(S.cat.items) do
|
||||
if Ops.isBadgeId(id) then ids[#ids + 1] = id end
|
||||
end
|
||||
return ids
|
||||
return Gen.badgeIds(S.save, S.cat)
|
||||
end
|
||||
|
||||
function Ops.toggleBadge(S, id)
|
||||
-- #515: badges are truthy inventory entries written as 1 by the in-game
|
||||
-- grant (checkVictoryRewards, src/world/OverworldController.lua) and by
|
||||
-- GenSave's .sav import; read and write that same shape here, or a badge
|
||||
-- earned in game reads as unowned and an editor-written boolean blows up
|
||||
-- Bag.add's `(inv[id] or 0) + qty` (src/inventory/Bag.lua).
|
||||
local on = S.save.inventory[id] and true or false
|
||||
S.save.inventory[id] = (not on) and 1 or nil
|
||||
return Ops.mark(S, ("%s %s"):format(id, on and "removed" or "earned"))
|
||||
local nowOn = Gen.toggleBadge(S.save, id)
|
||||
return Ops.mark(S, ("%s %s"):format(id, nowOn and "earned" or "removed"))
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------- events
|
||||
function Ops.setFlag(S, name, on)
|
||||
S.save.flags[name] = on and true or nil
|
||||
Gen.setFlag(S.save, name, on)
|
||||
return Ops.mark(S, ("%s = %s"):format(name, tostring(on and true or false)))
|
||||
end
|
||||
|
||||
@@ -801,17 +921,19 @@ end
|
||||
|
||||
-- -------------------------------------------------------------------- dex
|
||||
function Ops.dex(S)
|
||||
S.save.pokedex = S.save.pokedex or { seen = {}, owned = {} }
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
S.save.pokedex = S.save.pokedex or { seen = {}, [key] = {} }
|
||||
S.save.pokedex.seen = S.save.pokedex.seen or {}
|
||||
S.save.pokedex.owned = S.save.pokedex.owned or {}
|
||||
S.save.pokedex[key] = S.save.pokedex[key] or {}
|
||||
return S.save.pokedex
|
||||
end
|
||||
|
||||
function Ops.dexCounts(S)
|
||||
local dex = Ops.dex(S)
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
local seen, owned = 0, 0
|
||||
for _ in pairs(dex.seen) do seen = seen + 1 end
|
||||
for _ in pairs(dex.owned) do owned = owned + 1 end
|
||||
for _ in pairs(dex[key] or {}) do owned = owned + 1 end
|
||||
return seen, owned, #S.cat.species
|
||||
end
|
||||
|
||||
@@ -819,25 +941,28 @@ end
|
||||
-- the game's own rule, enforced here so a hand-edited dex stays legal.
|
||||
function Ops.dexSeen(S, species, on)
|
||||
local dex = Ops.dex(S)
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
dex.seen[species] = on and true or nil
|
||||
if not on then dex.owned[species] = nil end
|
||||
if not on then dex[key][species] = nil end
|
||||
return Ops.mark(S, ("%s %s"):format(species, on and "marked seen" or "cleared"))
|
||||
end
|
||||
|
||||
function Ops.dexOwned(S, species, on)
|
||||
local dex = Ops.dex(S)
|
||||
dex.owned[species] = on and true or nil
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
dex[key][species] = on and true or nil
|
||||
if on then dex.seen[species] = true end
|
||||
return Ops.mark(S, ("%s %s"):format(species, on and "marked owned" or "un-owned"))
|
||||
end
|
||||
|
||||
function Ops.dexStamp(S)
|
||||
local dex = Ops.dex(S)
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
local n = 0
|
||||
local function stamp(mon)
|
||||
if not dex.owned[mon.species] then n = n + 1 end
|
||||
if not dex[key][mon.species] then n = n + 1 end
|
||||
dex.seen[mon.species] = true
|
||||
dex.owned[mon.species] = true
|
||||
dex[key][mon.species] = true
|
||||
end
|
||||
for _, m in ipairs(S.save.party) do stamp(m) end
|
||||
for _, box in ipairs(S.save.boxes or {}) do
|
||||
@@ -855,9 +980,10 @@ end
|
||||
|
||||
function Ops.dexOwnAll(S)
|
||||
local dex = Ops.dex(S)
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
for _, species in ipairs(S.cat.species) do
|
||||
dex.seen[species] = true
|
||||
dex.owned[species] = true
|
||||
dex[key][species] = true
|
||||
end
|
||||
return Ops.mark(S, ("Marked all %d species owned"):format(#S.cat.species))
|
||||
end
|
||||
@@ -866,7 +992,8 @@ function Ops.dexClear(S)
|
||||
if not Ops.arm(S, "dex-clear", "Wipe the whole Pokedex? Click again to confirm") then
|
||||
return false
|
||||
end
|
||||
S.save.pokedex = { seen = {}, owned = {} }
|
||||
local key = Gen.dexOwnedKey(S.save)
|
||||
S.save.pokedex = { seen = {}, [key] = {} }
|
||||
return Ops.mark(S, "Pokedex wiped")
|
||||
end
|
||||
|
||||
@@ -927,6 +1054,11 @@ end
|
||||
-- OVERWORLD/PLATEAU tilesets, maps with connections, or fly spots the save
|
||||
-- has already visited.
|
||||
function Ops.isOutdoor(S, map)
|
||||
if not map or not map.def then return false end
|
||||
local Map2 = require("src.world.gen2.Map")
|
||||
if Gen.ofState(S) == 2 and Map2.isOutdoor then
|
||||
return Map2.isOutdoor(map.def) and true or false
|
||||
end
|
||||
if map.def.tileset == "OVERWORLD" or map.def.tileset == "PLATEAU" then
|
||||
return true
|
||||
end
|
||||
@@ -937,15 +1069,17 @@ end
|
||||
function Ops.setPlayerHere(S)
|
||||
local cell = S.mapClickCell
|
||||
if not cell then return Ops.say(S, "Click a cell first") end
|
||||
S.save.player.map = S.mapId
|
||||
S.save.player.x = cell.cx
|
||||
S.save.player.y = cell.cy
|
||||
Gen.setPlayerHere(S.save, S.mapId, cell.cx, cell.cy)
|
||||
return Ops.mark(S, ("Player set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
|
||||
end
|
||||
|
||||
function Ops.setLastOutdoor(S, map)
|
||||
local cell = S.mapClickCell
|
||||
if not cell then return Ops.say(S, "Click a cell first") end
|
||||
if Gen.ofState(S) == 2 then
|
||||
S.save.spawn = S.mapId
|
||||
return Ops.mark(S, ("spawn set to %s"):format(S.mapId))
|
||||
end
|
||||
if not Ops.isOutdoor(S, map) then
|
||||
return Ops.say(S, S.mapId .. " doesn't look outdoor (no connections, not visited)")
|
||||
end
|
||||
@@ -956,8 +1090,50 @@ end
|
||||
function Ops.setLastHeal(S)
|
||||
local cell = S.mapClickCell
|
||||
if not cell then return Ops.say(S, "Click a cell first") end
|
||||
if Gen.ofState(S) == 2 then
|
||||
S.save.spawn = S.mapId
|
||||
return Ops.mark(S, ("spawn set to %s"):format(S.mapId))
|
||||
end
|
||||
S.save.lastHeal = { map = S.mapId, x = cell.cx, y = cell.cy }
|
||||
return Ops.mark(S, ("lastHeal set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
|
||||
end
|
||||
|
||||
function Ops.setHeldItem(S, mon, id)
|
||||
if not mon then return false end
|
||||
if id == "" or id == nil then
|
||||
if not mon.item then return Ops.say(S, "No held item to clear") end
|
||||
local was = mon.item
|
||||
mon.item = nil
|
||||
syncPartyMailHeldItem(S, mon, was, nil)
|
||||
return Ops.mark(S, ("Cleared held item (%s)"):format(was))
|
||||
end
|
||||
if not S.data.items[id] then
|
||||
return Ops.say(S, ("%s is not an item"):format(tostring(id)))
|
||||
end
|
||||
local was = mon.item
|
||||
mon.item = id
|
||||
syncPartyMailHeldItem(S, mon, was, id)
|
||||
return Ops.mark(S, ("%s now holds %s"):format(mon.species, id))
|
||||
end
|
||||
|
||||
function Ops.setHappiness(S, mon, value)
|
||||
if not mon then return false end
|
||||
local want = clamp(math.floor(value), 0, 255)
|
||||
if want == (mon.happiness or 0) then
|
||||
return Ops.say(S, ("Happiness is already %d"):format(want))
|
||||
end
|
||||
mon.happiness = want
|
||||
return Ops.mark(S, ("%s happiness %d"):format(mon.species, want))
|
||||
end
|
||||
|
||||
function Ops.setPokerus(S, mon, value)
|
||||
if not mon then return false end
|
||||
local want = clamp(math.floor(value), 0, 255)
|
||||
if want == (mon.pokerus or 0) then
|
||||
return Ops.say(S, ("Pokerus is already %d"):format(want))
|
||||
end
|
||||
mon.pokerus = want
|
||||
return Ops.mark(S, ("%s pokerus byte %d"):format(mon.species, want))
|
||||
end
|
||||
|
||||
return Ops
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
-- in the selected box as a Lv5 mon built by the same MonOps path partyAdd
|
||||
-- uses, so its stats, exp and moves are consistent.
|
||||
|
||||
local BoxesMod = require("src.pokemon.Boxes")
|
||||
local PartyMod = require("src.pokemon.Party")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
@@ -33,12 +32,12 @@ local function drawStrip(S, Kit, boxes, x, y, stripW, h)
|
||||
local s = Kit.scale
|
||||
local pad = 16 * s
|
||||
Kit.card(x, y, stripW, h)
|
||||
Kit.caption(x + pad, y + pad, ("BOXES . %d"):format(BoxesMod.COUNT))
|
||||
Kit.caption(x + pad, y + pad, ("BOXES . %d"):format(Ops.boxCount(S)))
|
||||
local stripTop = y + pad + Kit.textHeight("caption") + 10 * s
|
||||
local stripInner = stripW - 2 * pad
|
||||
local bRowH = math.min(30 * s, math.max(22 * s,
|
||||
(h - (stripTop - y) - pad - (BoxesMod.COUNT - 1) * 6 * s) / BoxesMod.COUNT))
|
||||
for i = 1, BoxesMod.COUNT do
|
||||
(h - (stripTop - y) - pad - (Ops.boxCount(S) - 1) * 6 * s) / Ops.boxCount(S)))
|
||||
for i = 1, Ops.boxCount(S) do
|
||||
local ry = stripTop + (i - 1) * (bRowH + 6 * s)
|
||||
if ry + bRowH > y + h - pad then break end
|
||||
if Kit.row(x + pad, ry, stripInner, bRowH, i == S.selectedBox, PAL.blue, 9 * s) then
|
||||
@@ -52,7 +51,7 @@ local function drawStrip(S, Kit, boxes, x, y, stripW, h)
|
||||
ry + (bRowH - Kit.textHeight("tiny")) / 2, PAL.caption)
|
||||
local mx = x + pad + stripInner - 10 * s - countW - 8 * s - 44 * s
|
||||
Kit.meter(mx, ry + (bRowH - 5 * s) / 2, 44 * s, 5 * s,
|
||||
fill / BoxesMod.CAPACITY * 100, fill >= BoxesMod.CAPACITY and PAL.yellow or PAL.blue)
|
||||
fill / Ops.boxCapacity(S) * 100, fill >= Ops.boxCapacity(S) and PAL.yellow or PAL.blue)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +65,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
Kit.text("tab", ("Box %d"):format(S.selectedBox), gx,
|
||||
y + gpad + (headH - Kit.textHeight("tab")) / 2, PAL.heading)
|
||||
local titleW = Kit.textWidth("tab", ("Box %d"):format(S.selectedBox))
|
||||
Kit.text("mono", ("%d/%d"):format(#box, BoxesMod.CAPACITY),
|
||||
Kit.text("mono", ("%d/%d"):format(#box, Ops.boxCapacity(S)),
|
||||
gx + titleW + 14 * s, y + gpad + (headH - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||
local navW = 34 * s
|
||||
if Kit.stepper(gx + ginner - 2 * navW - 8 * s, y + gpad, navW, headH, "<",
|
||||
@@ -102,7 +101,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
end
|
||||
if Kit.button(gx + wdW + 10 * s, actY, addW, actH, addLabel,
|
||||
{ font = "small", radius = 9 * s,
|
||||
enabled = #box < BoxesMod.CAPACITY }) then
|
||||
enabled = #box < Ops.boxCapacity(S) }) then
|
||||
Ops.openBoxAddPicker(S, Kit)
|
||||
end
|
||||
if Kit.button(gx + ginner - relW, actY, relW, actH, relLabel,
|
||||
@@ -119,7 +118,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
-- of five slivers.
|
||||
local cols = math.max(2, math.min(COLS,
|
||||
math.floor((ginner + cellGap) / (86 * s + cellGap))))
|
||||
local rows = math.ceil(BoxesMod.CAPACITY / cols)
|
||||
local rows = math.ceil(Ops.boxCapacity(S) / cols)
|
||||
local cellW = math.max(0, (ginner - cellGap * (cols - 1)) / cols)
|
||||
-- floor at Kit's 26px tap target so a short window shrinks the cells but
|
||||
-- never inverts them (#715); overflow clips inside the grid body rather
|
||||
@@ -128,7 +127,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
math.min((gridH - cellGap * (rows - 1)) / rows, 110 * s))
|
||||
|
||||
Kit.pushClip(gx, gridTop, ginner, gridH)
|
||||
for i = 1, BoxesMod.CAPACITY do
|
||||
for i = 1, Ops.boxCapacity(S) do
|
||||
local cc = (i - 1) % cols
|
||||
local cr = math.floor((i - 1) / cols)
|
||||
local bx = gx + cc * (cellW + cellGap)
|
||||
@@ -220,7 +219,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
|
||||
S.selectedBox = Ops.clamp(S.selectedBox or 1, 1, BoxesMod.COUNT)
|
||||
S.selectedBox = Ops.clamp(S.selectedBox or 1, 1, Ops.boxCount(S))
|
||||
S.save.currentBox = S.selectedBox
|
||||
local boxes = Ops.boxes(S)
|
||||
local box = boxes[S.selectedBox]
|
||||
|
||||
@@ -145,7 +145,8 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local rx = cx + ci * (colW + colGap)
|
||||
local ry = gridTop + ri * (rowH + rowGap)
|
||||
local isSeen = dex.seen[id] == true
|
||||
local isOwned = dex.owned[id] == true
|
||||
local ownedKey = require("Gen").dexOwnedKey(S.save)
|
||||
local isOwned = dex[ownedKey] and dex[ownedKey][id] == true
|
||||
|
||||
Theme.row(rx, ry, colW, rowH, 9 * s, 0.6)
|
||||
local def = S.data.pokemon[id]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
local M = {}
|
||||
@@ -50,7 +51,7 @@ local function buildRows(S)
|
||||
if contains(name, filter) then
|
||||
rows[#rows + 1] = {
|
||||
label = name,
|
||||
checked = S.save.flags[name] == true,
|
||||
checked = Gen.getFlag(S.save, name),
|
||||
set = function(on) Ops.setFlag(S, name, on) end,
|
||||
}
|
||||
end
|
||||
@@ -107,7 +108,12 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
-- past the card edge.
|
||||
local pillH = 32 * s
|
||||
local px, py = cx, y + pad
|
||||
for _, t in ipairs(SUB_TABS) do
|
||||
local pills = SUB_TABS
|
||||
if Gen.of(S.save) == 2 then
|
||||
pills = { SUB_TABS[1] }
|
||||
if S.eventsTab ~= "flags" then S.eventsTab = "flags" end
|
||||
end
|
||||
for _, t in ipairs(pills) do
|
||||
local pw = Kit.textWidth("small", t.label) + 32 * s
|
||||
if px > cx and px + pw > cx + inner then
|
||||
px = cx
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
local M = {}
|
||||
@@ -46,7 +47,11 @@ local function quantityRow(S, Kit, x, y, w, h, id, qty, selected, onMinus, onPlu
|
||||
local qtyW = Kit.textWidth("monoRow", qtyText)
|
||||
Kit.textRight("monoRow", qtyText, bx - 10 * s,
|
||||
y + (h - Kit.textHeight("monoRow")) / 2, PAL.heading)
|
||||
Kit.text("mono", Kit.ellipsize("mono", id, bx - qtyW - 30 * s - (x + 10 * s)),
|
||||
local label = id
|
||||
if Gen.of(S.save) == 2 then
|
||||
label = (Bag.pocketOf(id, S.data) or "ITEM") .. " " .. id
|
||||
end
|
||||
Kit.text("mono", Kit.ellipsize("mono", label, bx - qtyW - 30 * s - (x + 10 * s)),
|
||||
x + 10 * s, y + (h - Kit.textHeight("mono")) / 2, PAL.text)
|
||||
return clicked
|
||||
end
|
||||
@@ -55,9 +60,13 @@ end
|
||||
-- Each card is a function of its own rect so the wide (three column) and the
|
||||
-- stacked (#715) layouts are the same drawing code with different geometry.
|
||||
|
||||
local function moneyHeight(Kit, s, pad)
|
||||
return pad * 2 + Kit.textHeight("caption") + 8 * s
|
||||
local function moneyHeight(Kit, s, pad, S)
|
||||
local h = pad * 2 + Kit.textHeight("caption") + 8 * s
|
||||
+ Kit.textHeight("headline") + 10 * s + 30 * s
|
||||
if S and Gen.of(S.save) == 2 then
|
||||
h = h + 28 * s
|
||||
end
|
||||
return h
|
||||
end
|
||||
|
||||
local function drawMoney(S, Kit, x, y, w, h)
|
||||
@@ -68,11 +77,19 @@ local function drawMoney(S, Kit, x, y, w, h)
|
||||
local maxW = 74 * s
|
||||
if Kit.button(x + w - pad - maxW, y + pad - 4 * s, maxW, 26 * s, "Max out",
|
||||
{ kind = "accent", font = "tiny", radius = 7 * s,
|
||||
enabled = (S.save.money or 0) < Ops.MONEY_MAX }) then
|
||||
enabled = Gen.money(S.save) < Ops.MONEY_MAX }) then
|
||||
Ops.maxMoney(S)
|
||||
end
|
||||
Kit.text("headline", ("$%d"):format(S.save.money or 0), x + pad,
|
||||
Kit.text("headline", ("$%d"):format(Gen.money(S.save)), x + pad,
|
||||
y + pad + Kit.textHeight("caption") + 8 * s, PAL.yellow)
|
||||
if Gen.of(S.save) == 2 then
|
||||
Kit.text("mono", ("COINS %d"):format(Gen.coins(S.save)), x + pad + 160 * s,
|
||||
y + pad + Kit.textHeight("caption") + 8 * s, PAL.muted)
|
||||
if Kit.button(x + w - pad - 74 * s, y + pad + 26 * s, 74 * s, 22 * s, "+100 coins",
|
||||
{ kind = "ghost", font = "tiny", radius = 6 * s }) then
|
||||
Ops.addCoins(S, 100)
|
||||
end
|
||||
end
|
||||
local mbY = y + h - pad - 30 * s
|
||||
local mbW = (w - 2 * pad - 3 * 8 * s) / 4
|
||||
for i, delta in ipairs(MONEY_STEPS) do
|
||||
@@ -99,10 +116,7 @@ local function drawBadges(S, Kit, x, y, w, h)
|
||||
Kit.card(x, y, w, h)
|
||||
local earned = 0
|
||||
for _, id in ipairs(badgeIds) do
|
||||
-- #515: truthy check, not `== true` -- the in-game grant path stores a
|
||||
-- number (see OverworldController.lua checkVictoryRewards), matching
|
||||
-- src/inventory/Badges.lua's own truthy read.
|
||||
if S.save.inventory[id] then earned = earned + 1 end
|
||||
if Gen.hasBadge(S.save, id) then earned = earned + 1 end
|
||||
end
|
||||
Kit.caption(x + pad, y + pad, "BADGES")
|
||||
Kit.textRight("mono", ("%d/%d"):format(earned, #badgeIds), x + w - pad,
|
||||
@@ -112,7 +126,7 @@ local function drawBadges(S, Kit, x, y, w, h)
|
||||
for i, id in ipairs(badgeIds) do
|
||||
local bc = (i - 1) % BADGE_COLS
|
||||
local br = math.floor((i - 1) / BADGE_COLS)
|
||||
local on = S.save.inventory[id]
|
||||
local on = Gen.hasBadge(S.save, id)
|
||||
local short = id:gsub("BADGE$", "")
|
||||
if Kit.chip(x + pad + bc * (bW + 7 * s), bTop + br * (28 * s + 7 * s),
|
||||
bW, 28 * s, Kit.ellipsize("micro", short, bW - 8 * s), on,
|
||||
@@ -253,7 +267,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
-- drag over their own bodies first.
|
||||
local off = Theme.clamp(S.itemsScroll or 0, 0,
|
||||
math.max(0, (S._itemsContentH or 0) - h))
|
||||
local moneyH = moneyHeight(Kit, s, pad)
|
||||
local moneyH = moneyHeight(Kit, s, pad, S)
|
||||
local badgeH = badgeHeight(S, Kit, s, pad)
|
||||
local pickH = 280 * s
|
||||
local listH = 300 * s
|
||||
@@ -278,7 +292,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
-- Money and badges are fixed-height so the picker gets every pixel left
|
||||
-- over: cycling through ~250 item ids in a two-row list was the thing that
|
||||
-- made the old panel unusable.
|
||||
local moneyH = moneyHeight(Kit, s, pad)
|
||||
local moneyH = moneyHeight(Kit, s, pad, S)
|
||||
local badgeH = badgeHeight(S, Kit, s, pad)
|
||||
drawMoney(S, Kit, x, y, leftW, moneyH)
|
||||
drawPicker(S, Kit, x, y + moneyH + gap, leftW, h - moneyH - badgeH - 2 * gap)
|
||||
|
||||
@@ -14,12 +14,18 @@ local MapLoader = require("src.world.MapLoader")
|
||||
local Warp = require("src.world.Warp")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
local MapBrowser = {}
|
||||
|
||||
local CELL = 16 -- the walk grid; a cell is 16px of map art
|
||||
|
||||
local function playerPos(S)
|
||||
local map, x, y = Gen.playerMap(S.save)
|
||||
return map, x or 0, y or 0
|
||||
end
|
||||
|
||||
local function clampZoom(z)
|
||||
if z < 1 then return 1 end
|
||||
if z > 4 then return 4 end
|
||||
@@ -38,7 +44,7 @@ MapBrowser.centerOn = centerOn
|
||||
|
||||
local function sortedMapIds(data)
|
||||
local ids = {}
|
||||
for id in pairs(data.maps) do ids[#ids + 1] = id end
|
||||
for id in pairs(Gen.maps(data)) do ids[#ids + 1] = id end
|
||||
table.sort(ids)
|
||||
return ids
|
||||
end
|
||||
@@ -52,7 +58,19 @@ local OUTSIDE_TILESETS = { OVERWORLD = true, PLATEAU = true }
|
||||
|
||||
local function goToWarp(S, warp)
|
||||
local def = warp.def
|
||||
local fromMap = S.data.maps[S.mapId]
|
||||
if Gen.of(S.save) == 2 then
|
||||
local dest = def.destMap or def.map
|
||||
if dest then
|
||||
S.mapId = dest
|
||||
S.mapClickCell = nil
|
||||
S._mapCenteredFor = dest
|
||||
S.status = "Followed warp to " .. tostring(dest)
|
||||
else
|
||||
S.status = "Warp has no destination map"
|
||||
end
|
||||
return
|
||||
end
|
||||
local fromMap = Gen.maps(S.data)[S.mapId]
|
||||
if fromMap and OUTSIDE_TILESETS[fromMap.tileset]
|
||||
and def.destMap ~= "LAST_MAP" and def.destMap ~= S.mapId then
|
||||
S.save.lastOutdoor = { id = S.mapId, x = def.x, y = def.y }
|
||||
@@ -125,22 +143,25 @@ local function drawOverlays(S, map)
|
||||
return cx * CELL - S.mapCamX, cy * CELL - S.mapCamY, CELL, CELL
|
||||
end
|
||||
love.graphics.setColor(0.27, 0.59, 1, 0.55)
|
||||
for _, wdef in ipairs(map.def.warps) do
|
||||
for _, wdef in ipairs(map.def.warps or {}) do
|
||||
love.graphics.rectangle("line", cellRect(wdef.x, wdef.y))
|
||||
end
|
||||
if S.save.player.map == S.mapId then
|
||||
local playerMap, px, py = playerPos(S)
|
||||
if playerMap == S.mapId then
|
||||
love.graphics.setColor(1, 0.36, 0.4, 0.9)
|
||||
love.graphics.rectangle("fill", cellRect(S.save.player.x, S.save.player.y))
|
||||
love.graphics.rectangle("fill", cellRect(px, py))
|
||||
end
|
||||
local heal = S.save.lastHeal
|
||||
if heal and heal.map == S.mapId then
|
||||
love.graphics.setColor(0.24, 0.88, 0.54, 0.9)
|
||||
love.graphics.rectangle("line", cellRect(heal.x, heal.y))
|
||||
end
|
||||
local out = S.save.lastOutdoor
|
||||
if out and out.id == S.mapId then
|
||||
love.graphics.setColor(1, 0.8, 0.02, 0.9)
|
||||
love.graphics.rectangle("line", cellRect(out.x, out.y))
|
||||
if Gen.of(S.save) ~= 2 then
|
||||
local heal = S.save.lastHeal
|
||||
if heal and heal.map == S.mapId then
|
||||
love.graphics.setColor(0.24, 0.88, 0.54, 0.9)
|
||||
love.graphics.rectangle("line", cellRect(heal.x, heal.y))
|
||||
end
|
||||
local out = S.save.lastOutdoor
|
||||
if out and out.id == S.mapId then
|
||||
love.graphics.setColor(1, 0.8, 0.02, 0.9)
|
||||
love.graphics.rectangle("line", cellRect(out.x, out.y))
|
||||
end
|
||||
end
|
||||
if S.mapClickCell then
|
||||
love.graphics.setColor(1, 1, 0.35, 0.95)
|
||||
@@ -239,9 +260,13 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
#ids, perPage)
|
||||
if Kit.button(lr.x + pad, gotoY, listInner, gotoH, "Go to save location",
|
||||
{ font = "small", radius = 9 * s }) then
|
||||
MapBrowser.select(S, S.save.player.map)
|
||||
Ops.say(S, ("Jumped to %s (%d,%d)"):format(S.save.player.map,
|
||||
S.save.player.x, S.save.player.y))
|
||||
local pmap, px, py = playerPos(S)
|
||||
if pmap then
|
||||
MapBrowser.select(S, pmap)
|
||||
Ops.say(S, ("Jumped to %s (%d,%d)"):format(pmap, px, py))
|
||||
else
|
||||
Ops.say(S, "No player location on this save")
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------- the viewport
|
||||
@@ -253,7 +278,32 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
Kit.text("monoBig", tostring(S.mapId), vx0,
|
||||
vr.y + vpad + (headH - Kit.textHeight("monoBig")) / 2, PAL.heading)
|
||||
|
||||
local ok, map = pcall(MapLoader.load, S.data, S.mapId)
|
||||
local ok, map
|
||||
if Gen.of(S.save) == 2 then
|
||||
local def = Gen.maps(S.data)[S.mapId]
|
||||
if def then
|
||||
local Map2 = require("src.world.gen2.Map")
|
||||
if type(def.width) ~= "number" or type(def.height) ~= "number" then
|
||||
ok, map = false, "incomplete map record (missing width/height)"
|
||||
else
|
||||
local tileset = Gen.tilesets(S.data)[def.tileset]
|
||||
ok, map = pcall(Map2.new, def, tileset or {})
|
||||
if ok and map and not map.renderer then
|
||||
local MapPreview = require("src.world.gen2.MapPreview")
|
||||
S._g2MapBaker = S._g2MapBaker or MapPreview.baker({
|
||||
tilesets = Gen.tilesets(S.data),
|
||||
gen2Roofs = S.data.gen2Roofs, roofs = S.data.roofs,
|
||||
gen2Palettes = S.data.gen2Palettes, palettes = S.data.palettes,
|
||||
})
|
||||
map.renderer = MapPreview.renderer(S._g2MapBaker, map)
|
||||
end
|
||||
end
|
||||
else
|
||||
ok, map = false, "unknown map"
|
||||
end
|
||||
else
|
||||
ok, map = pcall(MapLoader.load, S.data, S.mapId)
|
||||
end
|
||||
if not ok then
|
||||
Kit.text("mono", "Failed to load map: " .. tostring(map), vx0,
|
||||
vr.y + vpad + headH + 20 * s, PAL.red)
|
||||
@@ -279,12 +329,13 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
local zBtn = 32 * s
|
||||
local rightEdge = vx0 + vinner
|
||||
local zoomW = 2 * zBtn + 56 * s + 12 * s
|
||||
local pmap, px, py = playerPos(S)
|
||||
local showCenter = vinner >= zoomW + 10 * s + centerW + 160 * s
|
||||
if showCenter then
|
||||
if Kit.button(rightEdge - centerW, vr.y + vpad, centerW, headH, "Center on player",
|
||||
{ kind = "accent", font = "small", radius = 7 * s }) then
|
||||
if S.save.player.map == S.mapId then
|
||||
centerOn(S, S.save.player.x, S.save.player.y)
|
||||
if pmap == S.mapId then
|
||||
centerOn(S, px, py)
|
||||
Ops.say(S, "Centred on the player")
|
||||
else
|
||||
Ops.say(S, "Player isn't on this map")
|
||||
@@ -312,10 +363,11 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
-- the panel has laid itself out.
|
||||
if S._mapCenteredFor ~= S.mapId then
|
||||
S._mapCenteredFor = S.mapId
|
||||
if S.save.player.map == S.mapId then
|
||||
centerOn(S, S.save.player.x, S.save.player.y)
|
||||
if pmap == S.mapId then
|
||||
centerOn(S, px, py)
|
||||
else
|
||||
centerOn(S, map.widthCells / 2, map.heightCells / 2)
|
||||
centerOn(S, (map.widthCells or map.width or 10) / 2,
|
||||
(map.heightCells or map.height or 10) / 2)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -334,7 +386,24 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
love.graphics.push()
|
||||
love.graphics.translate(vx0, vy0)
|
||||
love.graphics.scale(S.mapZoom, S.mapZoom)
|
||||
map.renderer:draw(S.mapCamX, S.mapCamY)
|
||||
if map.renderer and map.renderer.draw then
|
||||
map.renderer:draw(S.mapCamX, S.mapCamY)
|
||||
else
|
||||
local wc = map.widthCells or ((map.width or 8) * 2)
|
||||
local hc = map.heightCells or ((map.height or 8) * 2)
|
||||
for cy = 0, hc - 1 do
|
||||
for cx = 0, wc - 1 do
|
||||
if (cx + cy) % 2 == 0 then
|
||||
love.graphics.setColor(0.18, 0.22, 0.32, 1)
|
||||
else
|
||||
love.graphics.setColor(0.14, 0.17, 0.26, 1)
|
||||
end
|
||||
love.graphics.rectangle("fill",
|
||||
cx * CELL - S.mapCamX, cy * CELL - S.mapCamY, CELL, CELL)
|
||||
end
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
drawOverlays(S, map)
|
||||
love.graphics.pop()
|
||||
love.graphics.setScissor()
|
||||
@@ -402,20 +471,31 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
Kit.caption(sx0 + pad, sr.y + pad, "SPAWN POINTS")
|
||||
local sTop = sr.y + pad + Kit.textHeight("caption") + 12 * s
|
||||
local sInner = sr.w - 2 * pad
|
||||
local player = S.save.player
|
||||
local out = S.save.lastOutdoor
|
||||
local heal = S.save.lastHeal
|
||||
local spawns = {
|
||||
{ key = "PLAYER", color = PAL.red,
|
||||
value = ("%s (%d,%d)"):format(player.map, player.x, player.y),
|
||||
set = function() Ops.setPlayerHere(S) end },
|
||||
{ key = "LAST HEAL", color = PAL.green,
|
||||
value = heal and ("%s (%d,%d)"):format(heal.map, heal.x, heal.y) or "unset",
|
||||
set = function() Ops.setLastHeal(S) end },
|
||||
{ key = "LAST OUTDOOR", color = PAL.yellow,
|
||||
value = out and ("%s (%d,%d)"):format(out.id, out.x, out.y) or "unset",
|
||||
set = function() Ops.setLastOutdoor(S, map) end },
|
||||
}
|
||||
local pmap2, px2, py2 = playerPos(S)
|
||||
local playerValue = pmap2 and ("%s (%d,%d)"):format(pmap2, px2, py2) or "unset"
|
||||
local spawns
|
||||
if Gen.of(S.save) == 2 then
|
||||
spawns = {
|
||||
{ key = "PLAYER", color = PAL.red, value = playerValue,
|
||||
set = function() Ops.setPlayerHere(S) end },
|
||||
{ key = "SPAWN", color = PAL.green,
|
||||
value = tostring(S.save.spawn or "SPAWN_HOME"),
|
||||
set = function() Ops.setLastHeal(S) end },
|
||||
}
|
||||
else
|
||||
local out = S.save.lastOutdoor
|
||||
local heal = S.save.lastHeal
|
||||
spawns = {
|
||||
{ key = "PLAYER", color = PAL.red, value = playerValue,
|
||||
set = function() Ops.setPlayerHere(S) end },
|
||||
{ key = "LAST HEAL", color = PAL.green,
|
||||
value = heal and ("%s (%d,%d)"):format(heal.map, heal.x, heal.y) or "unset",
|
||||
set = function() Ops.setLastHeal(S) end },
|
||||
{ key = "LAST OUTDOOR", color = PAL.yellow,
|
||||
value = out and ("%s (%d,%d)"):format(out.id, out.x, out.y) or "unset",
|
||||
set = function() Ops.setLastOutdoor(S, map) end },
|
||||
}
|
||||
end
|
||||
local spawnH = 62 * s
|
||||
for i, sp in ipairs(spawns) do
|
||||
local ry = sTop + (i - 1) * (spawnH + 8 * s)
|
||||
@@ -431,7 +511,7 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
||||
sx0 + pad + 12 * s, ry + spawnH - 10 * s - Kit.textHeight("mono"), PAL.muted)
|
||||
end
|
||||
|
||||
local noteY = sTop + 3 * (spawnH + 8 * s) + 6 * s
|
||||
local noteY = sTop + #spawns * (spawnH + 8 * s) + 6 * s
|
||||
Kit.textCenter("tiny",
|
||||
"Click a cell first. Warp cells follow the warp instead of selecting. " ..
|
||||
"Arrow keys / WASD pan, the wheel zooms.",
|
||||
|
||||
@@ -19,17 +19,26 @@
|
||||
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 = {
|
||||
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,
|
||||
@@ -96,7 +105,7 @@ local function drawLevelRow(S, Kit, mon, lx0, ly)
|
||||
end
|
||||
lx = lx + bw + 8 * s
|
||||
end
|
||||
Kit.text("mono", ("EXP %d"):format(mon.exp or 0), lx + 6 * s,
|
||||
Kit.text("mono", ("EXP %d"):format(Gen.exp(mon)), lx + 6 * s,
|
||||
ly + (lh - Kit.textHeight("mono")) / 2, PAL.muted)
|
||||
return lh
|
||||
end
|
||||
@@ -106,7 +115,7 @@ end
|
||||
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(mon.exp or 0))
|
||||
+ 6 * s + Kit.textWidth("mono", ("EXP %d"):format(Gen.exp(mon)))
|
||||
end
|
||||
|
||||
local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
|
||||
@@ -179,7 +188,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
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 " ..
|
||||
"Gen1 stat formulas, so HP and stats stay legal.",
|
||||
"stat formulas, so HP and stats stay legal.",
|
||||
x + (w - tw) / 2, y + h / 2 - Kit.textHeight("button"), tw, PAL.muted)
|
||||
return
|
||||
end
|
||||
@@ -220,10 +229,13 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
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
|
||||
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
|
||||
@@ -311,8 +323,9 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
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 * 4) / 5
|
||||
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)
|
||||
@@ -326,6 +339,40 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
|
||||
-- --------------------------------------------------- DVs | moves split
|
||||
local colY = statsY + cellH + 18 * s
|
||||
if Gen.ofState(S) == 2 then
|
||||
local extraY = colY
|
||||
Kit.caption(cx, extraY, "GOLD")
|
||||
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)
|
||||
colY = extraY + 22 * s
|
||||
end
|
||||
if narrow then
|
||||
-- stacked: DVs first, then moves, then the two actions side by side at
|
||||
-- full width (#715)
|
||||
|
||||
Reference in New Issue
Block a user