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 editorHost, editorVersion, editorWindow
|
||||||
local closeEditor -- forward declaration: openEditor hands it to the editor
|
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
|
-- The editor's modules use flat names (require("Kit"), require("Party")), so
|
||||||
-- their directories have to be on the require path. It must be
|
-- 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
|
-- 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 = Importer.saveNotice or {}
|
||||||
Importer.saveNotice[version] = { ok = false, text = text }
|
Importer.saveNotice[version] = { ok = false, text = text }
|
||||||
end
|
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 SaveData = require("src.core.SaveData")
|
||||||
local path = SaveData.slotDiskPath(version, slotId)
|
local path = SaveData.slotDiskPath(version, slotId)
|
||||||
if not path then
|
if not path then
|
||||||
@@ -320,14 +306,6 @@ function love.load(args)
|
|||||||
-- cache has to be mounted before the editor's Data:load.
|
-- cache has to be mounted before the editor's Data:load.
|
||||||
if editorMode then
|
if editorMode then
|
||||||
local version = os.getenv("POKEPORT_VERSION") or "red"
|
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.core.GameVersion").set(version)
|
||||||
require("src.import.CacheFs").mountVersion(version)
|
require("src.import.CacheFs").mountVersion(version)
|
||||||
addEditorRequirePath()
|
addEditorRequirePath()
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ rm -f "$OUTPUT"
|
|||||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||||
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
-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
|
if [ -n "$BUILD_INFO" ]; then
|
||||||
[ -f "$BUILD_INFO" ] || fail "missing build-info: $BUILD_INFO"
|
[ -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: 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: 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: 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: 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 "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
|
run_tier "T5 link (loopback lockstep)" "$LUA" tests/run_link_tests.lua
|
||||||
|
|||||||
@@ -98,10 +98,69 @@ function Mon.stats(baseStats, dvs, level, statExp)
|
|||||||
}
|
}
|
||||||
end
|
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)
|
function Mon.refreshStats(mon, data)
|
||||||
if type(mon) ~= "table" then return mon end
|
if type(mon) ~= "table" then return mon end
|
||||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||||
if not (def and def.baseStats) then return mon end
|
if not (def and def.baseStats) then return mon end
|
||||||
|
Mon.syncIdentity(mon, data)
|
||||||
-- engine/pokemon/move_mon.asm:1402
|
-- engine/pokemon/move_mon.asm:1402
|
||||||
local stats = Mon.stats(def.baseStats, mon.dvs, mon.level or 1, mon.statExp)
|
local stats = Mon.stats(def.baseStats, mon.dvs, mon.level or 1, mon.statExp)
|
||||||
mon.stats = stats
|
mon.stats = stats
|
||||||
|
|||||||
+5
-2
@@ -283,6 +283,9 @@ function Game2:continueGame(save)
|
|||||||
local modsDiff = SaveData.modsDiff(save, activeMods)
|
local modsDiff = SaveData.modsDiff(save, activeMods)
|
||||||
self.save = save
|
self.save = save
|
||||||
self:adoptSave(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
|
-- 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
|
-- 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.
|
-- save written before they moved out must not drag old values back in.
|
||||||
@@ -588,13 +591,13 @@ function Game2:useFieldItem(itemId)
|
|||||||
end
|
end
|
||||||
if not allowed then
|
if not allowed then
|
||||||
self:say(("%s can't learn %s!"):format(
|
self:say(("%s can't learn %s!"):format(
|
||||||
mon.nickname or mon.species or "?", moveName))
|
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, move in ipairs(mon.moves or {}) do
|
for _, move in ipairs(mon.moves or {}) do
|
||||||
if move.id == moveId then
|
if move.id == moveId then
|
||||||
self:say(("%s already knows %s!"):format(
|
self:say(("%s already knows %s!"):format(
|
||||||
mon.nickname or mon.species or "?", moveName))
|
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
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 CART_DRAG_SLOP = 8
|
||||||
local TAU = math.pi * 2
|
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)
|
local function cartridgeState(imp, version)
|
||||||
imp._cartridge = imp._cartridge or {}
|
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)
|
Theme.A.focus, 2, Theme.cardRadius() + 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local halfW, halfH = w / 2, h / 2
|
local meshPad = math.max(CART_MESH_PAD_MIN,
|
||||||
local depth = math.max(8, w * 0.14)
|
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)
|
local project = function(px, py, pz)
|
||||||
return cartProject(cx + pressX, cy + pressY, yaw, pitch,
|
return cartProject(cx + pressX, cy + pressY, yaw, pitch,
|
||||||
px * pressedScale, py * pressedScale, pz * pressedScale)
|
px * pressedScale, py * pressedScale, pz * pressedScale)
|
||||||
@@ -638,14 +646,8 @@ local function modStatusColor(status)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- MODS panel scope row: which game the list is answering for, plus dedicated Profile control (cycle + gear).
|
-- 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 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") } }
|
local options = { { id = nil, label = Strings("All games") } }
|
||||||
for _, version in ipairs(GameVersion.ORDER) do
|
for _, version in ipairs(GameVersion.ORDER) do
|
||||||
if imp.ready and imp.ready[version] then
|
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 }
|
{ id = version, label = GameVersion.info(version).label }
|
||||||
end
|
end
|
||||||
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
|
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar
|
||||||
local _, activeProf = LauncherMods.getProfiles()
|
local _, activeProf = LauncherMods.getProfiles()
|
||||||
@@ -690,9 +719,12 @@ local function buildModScopeRow(imp, x, y, w, m)
|
|||||||
})
|
})
|
||||||
|
|
||||||
if #options >= 2 then
|
if #options >= 2 then
|
||||||
for _, opt in ipairs(options) do
|
local avail = profX - gap - cx
|
||||||
local cw = Kit.textWidth("micro", opt.label) + math.floor(18 * m.s)
|
-- Chips stay when they all fit; otherwise they used to be skipped and
|
||||||
if cx + cw <= profX - gap then
|
-- 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,
|
if Kit.chip(cx, y, cw, h, opt.label, imp.modScope == opt.id, PAL.lineStrong,
|
||||||
"mod-scope-" .. tostring(opt.id or "all")) then
|
"mod-scope-" .. tostring(opt.id or "all")) then
|
||||||
local want = opt.id
|
local want = opt.id
|
||||||
@@ -701,6 +733,15 @@ local function buildModScopeRow(imp, x, y, w, m)
|
|||||||
end
|
end
|
||||||
cx = cx + cw + gap
|
cx = cx + cw + gap
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
return h + math.floor(8 * m.s)
|
return h + math.floor(8 * m.s)
|
||||||
@@ -740,8 +781,8 @@ local function setPage(imp, key, v)
|
|||||||
imp._pages[key] = v
|
imp._pages[key] = v
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A hand-drawn X, for the same reason drawCheck exists below: the UI font has
|
-- A hand-drawn X / check: the UI font has no guaranteed glyph for either,
|
||||||
-- no guaranteed glyph, and the launcher ships no icon asset for it.
|
-- and the launcher ships no icon asset for them.
|
||||||
local function drawCross(x, y, size, color)
|
local function drawCross(x, y, size, color)
|
||||||
love.graphics.push("all")
|
love.graphics.push("all")
|
||||||
love.graphics.setColor(color)
|
love.graphics.setColor(color)
|
||||||
@@ -752,6 +793,18 @@ local function drawCross(x, y, size, color)
|
|||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
end
|
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
|
-- ------------------------------------------------------------- header
|
||||||
-- Rail, logo row (settings and quit on the right), tab bar.
|
-- Rail, logo row (settings and quit on the right), tab bar.
|
||||||
-- Returns the y at which content may start. Its vertical arithmetic is
|
-- 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)
|
or tostring(version)
|
||||||
local ready = (not locked) and imp.ready[version] or false
|
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")
|
local titleH = Kit.textHeight("title")
|
||||||
Kit.text("title", Kit.ellipsize("title", gameName, w * 0.6), x, y, PAL.heading)
|
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 tagH = Kit.textHeight("micro") + math.floor(10 * m.s)
|
||||||
local tagX = x + Kit.textWidth("title", Kit.ellipsize("title", gameName, w * 0.6))
|
local tagX = x + Kit.textWidth("title", Kit.ellipsize("title", gameName, w * 0.6))
|
||||||
+ math.floor(12 * m.s)
|
+ 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
|
if ready then
|
||||||
local hint = Strings("(PRESS THE CART TO PLAY)")
|
local hint = Strings("(PRESS THE CART TO PLAY)")
|
||||||
local hintX = tagX + tagW + math.floor(10 * m.s)
|
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,
|
Kit.text("micro", Kit.ellipsize("micro", hint, hintW), hintX,
|
||||||
y + (titleH - Kit.textHeight("micro")) / 2, PAL.heading)
|
y + (titleH - Kit.textHeight("micro")) / 2, PAL.heading)
|
||||||
end
|
end
|
||||||
local cy = y + titleH + math.floor(12 * m.s)
|
-- Extra gap under the title when the cart is showing: 12px left the 3D
|
||||||
local remaining = availH - (titleH + math.floor(12 * m.s))
|
-- 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 gap = m.gap
|
||||||
local lx, lw, rx2, rw
|
local lx, lw, rx2, rw
|
||||||
@@ -1446,20 +1514,6 @@ local function currentSort(imp)
|
|||||||
return sortKey
|
return sortKey
|
||||||
end
|
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
|
-- One compact coloured checkbox for each game. The cartridge colour carries
|
||||||
-- the game identity even when the row is narrow.
|
-- the game identity even when the row is narrow.
|
||||||
local function modGameCheckbox(x, y, size, checked, game, id)
|
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 "
|
.. "it might have been tampered with. go to the discord to verify "
|
||||||
.. COMMUNITY_URL .. " (or click the logo above)"
|
.. 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
|
-- Pinned to the bottom of the window; returns the y it starts at, so the
|
||||||
-- panels above know how much room they have.
|
-- panels above know how much room they have.
|
||||||
-- Deliberately compact: at a large UI scale the footer is pure overhead
|
-- 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
|
-- 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.
|
-- link share one line and the trust warning is capped at a single line.
|
||||||
local function footerHeight(imp, m)
|
local function footerHeight(imp, m)
|
||||||
-- Top pad + mark/update row + gap + the FULL wrapped trust message +
|
-- Top pad + mark/update row + optional notes wrap row + gap + the FULL
|
||||||
-- bottom pad. The message wraps to as many lines as it needs: truncating
|
-- wrapped trust message + bottom pad. The message wraps to as many lines
|
||||||
-- a trust warning defeats its purpose, and the bottom pad is not optional
|
-- as it needs: truncating a trust warning defeats its purpose, and the
|
||||||
-- either (without it the last line sits flush on the window edge and its
|
-- bottom pad is not optional either (without it the last line sits flush
|
||||||
-- lower half clips off). The row is tapMin tall because the small update
|
-- on the window edge and its lower half clips off). The row is tapMin
|
||||||
-- button rides beside the mark.
|
-- tall because the small update button rides beside the mark.
|
||||||
local rowH = math.max(math.floor(22 * m.s), Kit.tapMin())
|
local f = footerLayout(imp, m, math.floor(130 * m.s))
|
||||||
return math.floor(8 * m.s) + rowH + math.floor(6 * m.s)
|
local h = math.floor(8 * m.s) + f.rowH + math.floor(6 * m.s)
|
||||||
+ Kit.wrapHeight("micro", TRUST_WARNING, m.contentW)
|
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)
|
+ math.floor(8 * m.s)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2009,19 +2089,17 @@ local function buildFooter(imp, m, y)
|
|||||||
local bw, bh = imp.bcg:getDimensions()
|
local bw, bh = imp.bcg:getDimensions()
|
||||||
local scale = math.min((130 * m.s) / bw, (22 * m.s) / bh)
|
local scale = math.min((130 * m.s) / bw, (22 * m.s) / bh)
|
||||||
local dw, dh = bw * scale, bh * scale
|
local dw, dh = bw * scale, bh * scale
|
||||||
local rowH = math.max(math.floor(22 * m.s), Kit.tapMin())
|
local f = footerLayout(imp, m, dw)
|
||||||
-- The mark and the small self-update control share the row, centred as a
|
local rowH, gap, chipH = f.rowH, f.gap, f.chipH
|
||||||
-- group. The updater moved down here from the header, where it overlapped
|
-- The mark, the small self-update control, and Patch notes share the row,
|
||||||
-- the wordmark on a phone; small on purpose, its glow still carries the
|
-- centred as a group. The updater moved down here from the header, where
|
||||||
-- "act on me" signal.
|
-- it overlapped the wordmark on a phone; small on purpose, its glow still
|
||||||
local upStatus, upLabel, upAction, upGlow = LauncherView._updateControl(imp)
|
-- carries the "act on me" signal. Notes wrap under the mark on a phone.
|
||||||
-- Kit.button insets its label 16*scale per side, so the width must budget
|
local topW = dw + (f.upStatus and (gap + f.uw) or 0)
|
||||||
-- more than that or the label ellipsizes ("Check for updat...").
|
if not f.wrap then topW = topW + gap + f.nw end
|
||||||
local uw = upStatus
|
local bx = m.x + math.floor((m.w - topW) / 2)
|
||||||
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 my = cy + math.floor((rowH - dh) / 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)
|
local hot = Kit.hover(bx, my, dw, dh)
|
||||||
love.graphics.setShader(imp.invertShader)
|
love.graphics.setShader(imp.invertShader)
|
||||||
love.graphics.setColor(1, 1, 1, hot and 1 or 0.85)
|
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
|
if Kit.press(bx, my, dw, dh) then
|
||||||
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
||||||
end
|
end
|
||||||
if upStatus then
|
local cx = bx + dw
|
||||||
btn(imp, bx + dw + math.floor(10 * m.s), cy, uw, rowH, "updater",
|
if f.upStatus then
|
||||||
upLabel, {
|
cx = cx + gap
|
||||||
kind = upGlow and "warn" or "ghost", font = "micro",
|
btn(imp, cx, chipY, f.uw, chipH, "updater",
|
||||||
glow = upGlow, action = upAction,
|
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
|
end
|
||||||
cy = cy + rowH + math.floor(6 * m.s)
|
|
||||||
-- The trust message wraps in full, each line centred under the mark, and
|
-- 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.
|
-- 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
|
-- 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 })
|
action = function() imp._sortPopup = nil end })
|
||||||
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
|
-- Category filter for FIND MODS. Two columns, because an index can list
|
||||||
-- enough categories to overflow a single stacked column on a short window.
|
-- enough categories to overflow a single stacked column on a short window.
|
||||||
local function buildFilterModal(imp, m)
|
local function buildFilterModal(imp, m)
|
||||||
@@ -3253,8 +3375,9 @@ end
|
|||||||
local function modalUp(imp)
|
local function modalUp(imp)
|
||||||
return (imp._settingsText or imp._settings or imp._rename
|
return (imp._settingsText or imp._settings or imp._rename
|
||||||
or imp._indexPrompt or imp._modConfirm or imp._modReleaseNotes
|
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._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._modHeaderActionsPopup or imp._profilesPopup or imp._singleProfileActions or imp._profileSavePrompt
|
||||||
or imp._profileRenamePrompt or imp._findEntry or imp._gameManage) ~= nil
|
or imp._profileRenamePrompt or imp._findEntry or imp._gameManage) ~= nil
|
||||||
end
|
end
|
||||||
@@ -3351,6 +3474,20 @@ local function buildModals(imp, m)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if imp._modConfirm then buildConfirmModal(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
|
if imp._modReleaseNotes then
|
||||||
local ModUpdate = require("src.mods.ModUpdate")
|
local ModUpdate = require("src.mods.ModUpdate")
|
||||||
local n = imp._modReleaseNotes
|
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._profilesPopup then buildProfilesModal(imp, m) return true end
|
||||||
if imp._modHeaderActionsPopup then buildModHeaderActionsModal(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._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._filterPopup then buildFilterModal(imp, m) return true end
|
||||||
if imp._indexManage then buildIndexesModal(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
|
if imp._modActions then buildModActionsModal(imp, m) return true end
|
||||||
|
|||||||
@@ -2820,7 +2820,7 @@ function RomImporter:keypressed(key)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self._modConfirm or self._modVersions or self._modReleaseNotes
|
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
|
-- Focus navigation belongs to the visible modal as well as the launcher
|
||||||
-- beneath it. Route arrows and an already-armed confirm before this guard
|
-- beneath it. Route arrows and an already-armed confirm before this guard
|
||||||
-- returns; unarmed Enter still falls through to the modal guard. Keep this
|
-- returns; unarmed Enter still falls through to the modal guard. Keep this
|
||||||
@@ -2833,6 +2833,8 @@ function RomImporter:keypressed(key)
|
|||||||
self._findDetails = nil
|
self._findDetails = nil
|
||||||
elseif self._modReleaseNotes then
|
elseif self._modReleaseNotes then
|
||||||
self._modReleaseNotes = nil
|
self._modReleaseNotes = nil
|
||||||
|
elseif self._appPatchNotes then
|
||||||
|
self._appPatchNotes = nil
|
||||||
else
|
else
|
||||||
self._modConfirm = nil
|
self._modConfirm = nil
|
||||||
self._modVersions = nil
|
self._modVersions = nil
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function HallOfFame.monPlacements(mon, def)
|
|||||||
put(out, genderGlyph(mon.gender), 18, 13)
|
put(out, genderGlyph(mon.gender), 18, 13)
|
||||||
-- (8,14) is a bare '/', so the nickname starts at (9,14).
|
-- (8,14) is a bare '/', so the nickname starts at (9,14).
|
||||||
put(out, "/", 8, 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)
|
put(out, levelText(mon.level), 1, 16)
|
||||||
end
|
end
|
||||||
-- '<ID>' '№' '/' at (7,16), (8,16), (9,16), then five digits at (10,16).
|
-- '<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 not allowed then
|
||||||
if game.say then
|
if game.say then
|
||||||
game:say(("%s can't learn %s!"):format(
|
game:say(("%s can't learn %s!"):format(
|
||||||
mon.nickname or mon.species or "?", moveName))
|
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -638,7 +638,7 @@ function PackMenu:openTeachParty(row)
|
|||||||
if move.id == moveId then
|
if move.id == moveId then
|
||||||
if game.say then
|
if game.say then
|
||||||
game:say(("%s already knows %s!"):format(
|
game:say(("%s already knows %s!"):format(
|
||||||
mon.nickname or mon.species or "?", moveName))
|
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
+12
-3
@@ -71,6 +71,9 @@ function Check.parseRelease(jsonText, Json)
|
|||||||
payloadName = payloadName,
|
payloadName = payloadName,
|
||||||
payload = Check.pickAsset(doc.assets, payloadName),
|
payload = Check.pickAsset(doc.assets, payloadName),
|
||||||
sums = Check.pickAsset(doc.assets, "sha256sums.txt"),
|
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
|
end
|
||||||
|
|
||||||
@@ -135,6 +138,10 @@ local function drain()
|
|||||||
if stateCh then
|
if stateCh then
|
||||||
local msg = stateCh:pop()
|
local msg = stateCh:pop()
|
||||||
while msg do
|
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
|
cache = msg
|
||||||
msg = stateCh:pop()
|
msg = stateCh:pop()
|
||||||
end
|
end
|
||||||
@@ -158,11 +165,11 @@ function Check.start()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
requested = true
|
requested = true
|
||||||
cache = { status = "checking" }
|
cache = { status = "checking", notes = cache.notes, latest = cache.latest }
|
||||||
cmdCh:push({ cmd = "check" })
|
cmdCh:push({ cmd = "check" })
|
||||||
end
|
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.
|
-- idle | checking | uptodate | available | downloading | ready | needs_full | error.
|
||||||
function Check.state()
|
function Check.state()
|
||||||
drain()
|
drain()
|
||||||
@@ -171,6 +178,7 @@ function Check.state()
|
|||||||
latest = cache.latest,
|
latest = cache.latest,
|
||||||
progress = cache.progress,
|
progress = cache.progress,
|
||||||
error = cache.error,
|
error = cache.error,
|
||||||
|
notes = cache.notes,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -180,7 +188,8 @@ function Check.download()
|
|||||||
drain()
|
drain()
|
||||||
if not cmdCh then return end
|
if not cmdCh then return end
|
||||||
if cache.status ~= "available" 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" })
|
cmdCh:push({ cmd = "download" })
|
||||||
end
|
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 cmdCh = love.thread.getChannel("update_check_cmd")
|
||||||
local stateCh = love.thread.getChannel("update_check_state")
|
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 osName = (love.system and love.system.getOS and love.system.getOS()) or ""
|
||||||
local isWindows = osName == "Windows"
|
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
|
end
|
||||||
|
|
||||||
local modalFields = {
|
local modalFields = {
|
||||||
"_modConfirm", "_modVersions", "_modReleaseNotes", "_findDetails",
|
"_modConfirm", "_modVersions", "_modReleaseNotes", "_appPatchNotes", "_findDetails",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, field in ipairs(modalFields) do
|
for _, field in ipairs(modalFields) do
|
||||||
@@ -77,6 +77,12 @@ do
|
|||||||
imp:keypressed("escape")
|
imp:keypressed("escape")
|
||||||
eq(imp._modReleaseNotes, nil, "Escape closes release notes")
|
eq(imp._modReleaseNotes, nil, "Escape closes release notes")
|
||||||
end
|
end
|
||||||
|
do
|
||||||
|
resetFocus()
|
||||||
|
local imp = importer("_appPatchNotes")
|
||||||
|
imp:keypressed("escape")
|
||||||
|
eq(imp._appPatchNotes, nil, "Escape closes patch notes")
|
||||||
|
end
|
||||||
do
|
do
|
||||||
resetFocus()
|
resetFocus()
|
||||||
local imp = importer("_modVersions")
|
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.url, "http://x/love", "payload asset url picked")
|
||||||
eq(rel.payload.size, 12345, "payload asset size picked")
|
eq(rel.payload.size, 12345, "payload asset size picked")
|
||||||
eq(rel.sums.url, "http://x/sums", "sums asset url 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
|
-- 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
|
-- 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.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.url, "http://x/sums", "parseRelease picks the sums asset url")
|
||||||
eq(rel.sums.size, 99, "parseRelease picks the sums asset size")
|
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
|
-- 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
|
-- routes to a full reinstall rather than an in-place update
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
-- tests/save_editor_task7_tests.lua
|
-- tests/save_editor_task7_tests.lua
|
||||||
-- tests/save_editor_task8_tests.lua
|
-- tests/save_editor_task8_tests.lua
|
||||||
-- tests/save_editor_mod_tests.lua
|
-- tests/save_editor_mod_tests.lua
|
||||||
|
-- tests/save_editor_gen2_tests.lua
|
||||||
-- See tools/save-editor/README.md for the full list.
|
-- See tools/save-editor/README.md for the full list.
|
||||||
--
|
--
|
||||||
-- All of them drive tools/save-editor/Ops.lua rather than clicking pixel
|
-- 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 lua = (arg and arg[-1]) or "luajit"
|
||||||
local status = os.execute(("%q tests/save_editor_mod_tests.lua"):format(lua))
|
local status = os.execute(("%q tests/save_editor_mod_tests.lua"):format(lua))
|
||||||
check(status == 0 or status == true, "save_editor_mod_tests suite")
|
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
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------- input hold regressions
|
-- ---------------------------------------------- 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)
|
local report = SaveData.validate(probe, Data)
|
||||||
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
check(#report.lostMons == 0 and probe.party[1].species == "EDITMON",
|
||||||
"validate keeps the modded mon while the mod is enabled")
|
"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)
|
end)
|
||||||
|
|
||||||
os.remove(MOD_ROOT .. "/main.lua")
|
os.remove(MOD_ROOT .. "/main.lua")
|
||||||
@@ -154,7 +189,7 @@ os.remove(tmpPath)
|
|||||||
love.filesystem = savedFS
|
love.filesystem = savedFS
|
||||||
-- leave shared singletons the way we found them (the fixture merged one
|
-- leave shared singletons the way we found them (the fixture merged one
|
||||||
-- record into Data.pokemon)
|
-- record into Data.pokemon)
|
||||||
Data.pokemon.EDITMON = nil
|
if Data.pokemon then Data.pokemon.EDITMON = nil end
|
||||||
Assets.loader = savedBridge
|
Assets.loader = savedBridge
|
||||||
Assets.invalidate()
|
Assets.invalidate()
|
||||||
Runtime.install(savedEvents, savedHooks, savedErrors)
|
Runtime.install(savedEvents, savedHooks, savedErrors)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ package.path = "./?.lua;./?/init.lua;" .. package.path
|
|||||||
local Flags = dofile("tools/goldwalk/flags.lua")
|
local Flags = dofile("tools/goldwalk/flags.lua")
|
||||||
|
|
||||||
local ROOT = arg[1] or "../pokegold"
|
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 events, _ = Flags.parse(ROOT .. "/constants/event_flags.asm")
|
||||||
local engine, _ = Flags.parse(ROOT .. "/constants/engine_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 Kit = require("Kit")
|
||||||
local Theme = require("Theme")
|
local Theme = require("Theme")
|
||||||
local Ops = require("Ops")
|
local Ops = require("Ops")
|
||||||
|
local Gen = require("Gen")
|
||||||
local PadInput = require("PadInput")
|
local PadInput = require("PadInput")
|
||||||
local PAL = Theme.PAL
|
local PAL = Theme.PAL
|
||||||
|
|
||||||
@@ -87,56 +88,43 @@ local function applyLoaded(path, statusVerb)
|
|||||||
if save then
|
if save then
|
||||||
S.save = save
|
S.save = save
|
||||||
S.status = statusVerb .. " " .. path
|
S.status = statusVerb .. " " .. path
|
||||||
S.mapId = save.player.map
|
|
||||||
S.loadError = false
|
S.loadError = false
|
||||||
S.allowSave = true
|
S.allowSave = true
|
||||||
elseif existed then
|
elseif existed then
|
||||||
-- File is present but SaveIO.load couldn't decode it: treat it as a
|
S.save = Gen.newGame(S.version)
|
||||||
-- 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.status = "Corrupt save at " .. path .. " (" .. tostring(err) ..
|
S.status = "Corrupt save at " .. path .. " (" .. tostring(err) ..
|
||||||
"), Save disabled, use Reload after fixing the file"
|
"), Save disabled, use Reload after fixing the file"
|
||||||
S.mapId = S.save.player.map
|
|
||||||
S.loadError = true
|
S.loadError = true
|
||||||
S.allowSave = false
|
S.allowSave = false
|
||||||
else
|
else
|
||||||
S.save = require("src.core.SaveData").newGame()
|
S.save = Gen.newGame(S.version)
|
||||||
S.status = "No save at " .. path .. " (" .. tostring(err) ..
|
S.status = "No save at " .. path .. " (" .. tostring(err) ..
|
||||||
"), editing new game stub"
|
"), editing new game stub"
|
||||||
S.mapId = S.save.player.map
|
|
||||||
S.loadError = false
|
S.loadError = false
|
||||||
S.allowSave = true
|
S.allowSave = true
|
||||||
end
|
end
|
||||||
|
local mapId = Gen.playerMap(S.save)
|
||||||
|
S.mapId = mapId
|
||||||
S.dirty = false
|
S.dirty = false
|
||||||
S._quitArmed = false
|
S._quitArmed = false
|
||||||
S._openArmed = false
|
S._openArmed = false
|
||||||
S.editingMon = nil
|
S.editingMon = nil
|
||||||
Ops.disarm(S)
|
Ops.disarm(S)
|
||||||
local boxes = require("src.pokemon.Boxes").ensure(S.save)
|
Gen.ensureBoxes(S.save)
|
||||||
-- Imported .sav box mons have no stat block (box_struct stops before
|
Gen.hydrateSave(Data, S.save)
|
||||||
-- 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")
|
|
||||||
local probe = require("src.mods.Merge").deepCopy(S.save)
|
local probe = require("src.mods.Merge").deepCopy(S.save)
|
||||||
S.validation = SaveData.validate(probe, Data)
|
S.validation = Gen.validate(probe, Data)
|
||||||
if not SaveData.emptyReport(S.validation) then
|
if not Gen.emptyReport(S.save, S.validation) then
|
||||||
S.status = S.status .. string.format(", game would quarantine: %d mons, %d items, %d maps",
|
if Gen.of(S.save, S.version) == 2 then
|
||||||
#S.validation.lostMons, #S.validation.lostItems, #S.validation.remappedMaps)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -152,6 +140,9 @@ function App.load(pathOverride, opts)
|
|||||||
S.slotId = opts.slotId
|
S.slotId = opts.slotId
|
||||||
S.embedded = opts.embedded or false
|
S.embedded = opts.embedded or false
|
||||||
S.onClose = opts.onClose
|
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
|
-- the same mod set the game loads, merged into Data before the catalogs
|
||||||
-- build, so modded species/items/moves are editable and MonOps stops
|
-- build, so modded species/items/moves are editable and MonOps stops
|
||||||
-- asserting on them
|
-- asserting on them
|
||||||
@@ -163,6 +154,10 @@ function App.load(pathOverride, opts)
|
|||||||
-- loaded at least once, so it doubles as the "needs evicting" marker.
|
-- loaded at least once, so it doubles as the "needs evicting" marker.
|
||||||
if Data._pristineKeys then Data:unloadGenerated() end
|
if Data._pristineKeys then Data:unloadGenerated() end
|
||||||
Data:load()
|
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")
|
local ModLoader = require("src.mods.Loader")
|
||||||
mods = ModLoader.new()
|
mods = ModLoader.new()
|
||||||
mods:load(Data)
|
mods:load(Data)
|
||||||
@@ -174,9 +169,16 @@ function App.load(pathOverride, opts)
|
|||||||
for _, mod in ipairs(S.mods:status().loaded) do
|
for _, mod in ipairs(S.mods:status().loaded) do
|
||||||
modRoots[#modRoots + 1] = mod.path
|
modRoots[#modRoots + 1] = mod.path
|
||||||
end
|
end
|
||||||
S.events = Catalog.scrapeEvents("data/scripts", "data/generated/trainer_headers.lua",
|
if Gen.of(nil, opts.version) == 2 or require("src.core.GameVersion").generation() == 2 then
|
||||||
nil, modRoots)
|
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")
|
applyLoaded(pathOverride or SaveIO.defaultPath(), "Loaded")
|
||||||
|
if Gen.of(S.save, S.version) == 2 then
|
||||||
|
S.events = Catalog.goldEventList(modRoots)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Switch to another save file (Open button, drag-drop, or --save arg).
|
-- Switch to another save file (Open button, drag-drop, or --save arg).
|
||||||
@@ -582,11 +584,9 @@ local function tabCount(id)
|
|||||||
return tostring(n)
|
return tostring(n)
|
||||||
elseif id == "items" then
|
elseif id == "items" then
|
||||||
local Bag = require("src.inventory.Bag")
|
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
|
elseif id == "events" then
|
||||||
local n = 0
|
return tostring(Gen.flagCount(S.save))
|
||||||
for _ in pairs(S.save.flags or {}) do n = n + 1 end
|
|
||||||
return tostring(n)
|
|
||||||
elseif id == "map" then
|
elseif id == "map" then
|
||||||
-- map ids run long (REDS_HOUSE_2F); the rail is a summary, not a label
|
-- 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)
|
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,
|
-- 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.
|
-- so the rail can reserve the pill's width before laying out the tiles.
|
||||||
local function validationPill()
|
local function validationPill()
|
||||||
local SaveData = require("src.core.SaveData")
|
|
||||||
local report = S.validation
|
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
|
return "Save validates clean", PAL.green, nil, true
|
||||||
end
|
end
|
||||||
local parts = {}
|
local parts = {}
|
||||||
local target
|
local target
|
||||||
local function add(n, singular, plural, tab)
|
local function add(n, singular, plural, tab)
|
||||||
|
n = n or 0
|
||||||
if n <= 0 then return end
|
if n <= 0 then return end
|
||||||
parts[#parts + 1] = ("%d %s"):format(n, n == 1 and singular or plural)
|
parts[#parts + 1] = ("%d %s"):format(n, n == 1 and singular or plural)
|
||||||
target = target or tab
|
target = target or tab
|
||||||
end
|
end
|
||||||
add(#report.lostMons, "mon", "mons", "party")
|
if Gen.of(S.save, S.version) == 2 then
|
||||||
add(#report.lostItems, "item", "items", "items")
|
add(#(report.lostScriptMem or {}), "script byte", "script bytes", "events")
|
||||||
add(#report.remappedMaps, "map", "maps", "map")
|
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
|
return "Would quarantine " .. table.concat(parts, ", "), PAL.yellow, target, false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local dirs = { scriptDir }
|
local dirs = {}
|
||||||
|
if scriptDir then dirs[#dirs + 1] = scriptDir end
|
||||||
for _, dir in ipairs(extraDirs or {}) do
|
for _, dir in ipairs(extraDirs or {}) do
|
||||||
dirs[#dirs + 1] = dir
|
dirs[#dirs + 1] = dir
|
||||||
end
|
end
|
||||||
@@ -110,4 +111,25 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
|
|||||||
return sortedKeys(found)
|
return sortedKeys(found)
|
||||||
end
|
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
|
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 = {}
|
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)
|
return Pokemon.new(data, species, level)
|
||||||
end
|
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]
|
local def = data.pokemon[mon.species]
|
||||||
assert(def, "unknown species")
|
assert(def, "unknown species")
|
||||||
mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp)
|
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))
|
mon.hp = math.max(0, math.min(mon.hp or mon.stats.hp, mon.stats.hp))
|
||||||
end
|
end
|
||||||
|
|
||||||
function MonOps.setLevel(data, mon, level)
|
function MonOps.setLevel(data, mon, level, gen)
|
||||||
level = math.max(1, math.min(100, math.floor(level)))
|
level = math.max(1, math.min(100, math.floor(level)))
|
||||||
local def = data.pokemon[mon.species]
|
local def = data.pokemon[mon.species]
|
||||||
mon.level = level
|
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)
|
mon.exp = Growth.expForLevel(def.growthRate, level)
|
||||||
MonOps.recalc(data, mon)
|
MonOps.recalc(data, mon, gen)
|
||||||
end
|
end
|
||||||
|
|
||||||
function MonOps.setMove(data, mon, slot, moveId)
|
function MonOps.setMove(data, mon, slot, moveId)
|
||||||
@@ -32,6 +49,7 @@ function MonOps.setMove(data, mon, slot, moveId)
|
|||||||
id = moveId,
|
id = moveId,
|
||||||
pp = mdef.pp + ((mon.moves[slot] and mon.moves[slot].ppUps) or 0) * math.floor(mdef.pp / 5),
|
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,
|
ppUps = mon.moves[slot] and mon.moves[slot].ppUps or nil,
|
||||||
|
maxPp = mdef.pp,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,19 +60,38 @@ function MonOps.syncHpDv(dvs)
|
|||||||
return dvs
|
return dvs
|
||||||
end
|
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)))
|
mon.dvs[key] = math.max(0, math.min(15, math.floor(value)))
|
||||||
if key ~= "hp" then
|
if key ~= "hp" then
|
||||||
MonOps.syncHpDv(mon.dvs)
|
MonOps.syncHpDv(mon.dvs)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Keep level; resync exp to the species growth curve (species changes).
|
-- 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")
|
assert(data.pokemon[species], "unknown species")
|
||||||
mon.species = 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
|
end
|
||||||
|
|
||||||
return MonOps
|
return MonOps
|
||||||
|
|||||||
+246
-70
@@ -17,6 +17,7 @@ local BoxesMod = require("src.pokemon.Boxes")
|
|||||||
local Bag = require("src.inventory.Bag")
|
local Bag = require("src.inventory.Bag")
|
||||||
local MonOps = require("MonOps")
|
local MonOps = require("MonOps")
|
||||||
local Charmap = require("src.save_convert.data.charmap")
|
local Charmap = require("src.save_convert.data.charmap")
|
||||||
|
local Gen = require("Gen")
|
||||||
|
|
||||||
local Ops = {}
|
local Ops = {}
|
||||||
|
|
||||||
@@ -35,6 +36,62 @@ local function clamp(n, lo, hi)
|
|||||||
end
|
end
|
||||||
Ops.clamp = clamp
|
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()
|
local function now()
|
||||||
if love and love.timer and love.timer.getTime then
|
if love and love.timer and love.timer.getTime then
|
||||||
return love.timer.getTime()
|
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))
|
return Ops.say(S, ("Party is full (%d/%d)"):format(#S.save.party, PartyMod.MAX))
|
||||||
end
|
end
|
||||||
local species = S.cat.species[1]
|
local species = S.cat.species[1]
|
||||||
local mon = MonOps.create(S.data, species, 5)
|
local mon = createMon(S, species, 5)
|
||||||
mon.ot = S.save.player.name
|
|
||||||
mon.otId = S.save.player.id
|
|
||||||
table.insert(S.save.party, mon)
|
table.insert(S.save.party, mon)
|
||||||
S.selectedParty = #S.save.party
|
S.selectedParty = #S.save.party
|
||||||
S.editingMon = mon
|
S.editingMon = mon
|
||||||
@@ -129,6 +184,11 @@ function Ops.partyRemove(S)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
table.remove(S.save.party, index)
|
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
|
if S.editingMon == mon then S.editingMon = nil end
|
||||||
S.selectedParty = clamp(index, 1, math.max(#S.save.party, 1))
|
S.selectedParty = clamp(index, 1, math.max(#S.save.party, 1))
|
||||||
S.editingMon = S.save.party[S.selectedParty]
|
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")
|
return Ops.say(S, delta < 0 and "Already the lead mon" or "Already the last mon")
|
||||||
end
|
end
|
||||||
party[i], party[j] = party[j], party[i]
|
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
|
S.selectedParty = j
|
||||||
return Ops.mark(S, ("Moved %s to slot %d"):format(party[j].species, j))
|
return Ops.mark(S, ("Moved %s to slot %d"):format(party[j].species, j))
|
||||||
end
|
end
|
||||||
@@ -157,7 +220,7 @@ function Ops.setLevel(S, mon, level)
|
|||||||
if want == mon.level then
|
if want == mon.level then
|
||||||
return Ops.say(S, want == 1 and "Level is already 1" or "Level is already 100")
|
return Ops.say(S, want == 1 and "Level is already 1" or "Level is already 100")
|
||||||
end
|
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))
|
return Ops.mark(S, ("%s is now Lv%d"):format(mon.species, mon.level))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -172,15 +235,23 @@ end
|
|||||||
-- instead of trusting the list: without this, picking such a species walked
|
-- 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
|
-- Stats.calc into `speciesDef.baseStats[key]` on a nil and took the window
|
||||||
-- down (#541).
|
-- 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)
|
function Ops.speciesUsable(S, id)
|
||||||
local def = id and S.data.pokemon[id]
|
local def = id and S.data.pokemon[id]
|
||||||
if type(def) ~= "table" or type(def.baseStats) ~= "table" then return false end
|
if type(def) ~= "table" or type(def.baseStats) ~= "table" then return false end
|
||||||
for _, key in ipairs(BASE_STAT_KEYS) do
|
return baseStatsComplete(def.baseStats, BASE_STAT_KEYS_G1)
|
||||||
if type(def.baseStats[key]) ~= "number" then return false end
|
or baseStatsComplete(def.baseStats, BASE_STAT_KEYS_G2)
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The one funnel every species change goes through (the picker, the stepper,
|
-- The one funnel every species change goes through (the picker, the stepper,
|
||||||
@@ -199,14 +270,19 @@ function Ops.setSpecies(S, mon, id)
|
|||||||
end
|
end
|
||||||
-- MonOps.recalc replaces mon.stats with a fresh table rather than editing
|
-- MonOps.recalc replaces mon.stats with a fresh table rather than editing
|
||||||
-- it in place, so holding the old reference is a real rollback.
|
-- it in place, so holding the old reference is a real rollback.
|
||||||
local wasSpecies, wasLevel, wasExp = mon.species, mon.level, mon.exp
|
local wasSpecies, wasLevel, wasExp, wasExperience = mon.species, mon.level, mon.exp, mon.experience
|
||||||
local wasStats, wasHp = mon.stats, mon.hp
|
local wasStats, wasHp, wasName = mon.stats, mon.hp, mon.name
|
||||||
local ok, err = pcall(MonOps.setSpecies, S.data, mon, id)
|
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
|
if not ok then
|
||||||
mon.species, mon.level, mon.exp = wasSpecies, wasLevel, wasExp
|
mon.species, mon.level, mon.exp, mon.experience = wasSpecies, wasLevel, wasExp, wasExperience
|
||||||
mon.stats, mon.hp = wasStats, wasHp
|
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)))
|
return Ops.say(S, ("Could not set %s: %s"):format(tostring(id), tostring(err)))
|
||||||
end
|
end
|
||||||
|
syncPartyMailSpecies(S, mon)
|
||||||
return Ops.mark(S, ("Species set to %s"):format(id))
|
return Ops.mark(S, ("Species set to %s"):format(id))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -313,9 +389,9 @@ end
|
|||||||
-- the target is the box, not a mon.
|
-- the target is the box, not a mon.
|
||||||
function Ops.openBoxAddPicker(S, Kit)
|
function Ops.openBoxAddPicker(S, Kit)
|
||||||
local box = Ops.boxes(S)[S.selectedBox]
|
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)")
|
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||||
end
|
end
|
||||||
S.speciesPicker = { query = "", offset = 0, opened = true, mode = "box-add" }
|
S.speciesPicker = { query = "", offset = 0, opened = true, mode = "box-add" }
|
||||||
if Kit then Kit.focus = "species-picker" end -- soft keyboard rises (#529)
|
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.
|
-- box mon and a party mon born in the editor are indistinguishable.
|
||||||
function Ops.boxAddSpecies(S, id)
|
function Ops.boxAddSpecies(S, id)
|
||||||
local box = Ops.boxes(S)[S.selectedBox]
|
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)")
|
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||||
end
|
end
|
||||||
if not Ops.speciesUsable(S, id) then
|
if not Ops.speciesUsable(S, id) then
|
||||||
return Ops.say(S, ("%s has no usable base stats, cannot add it")
|
return Ops.say(S, ("%s has no usable base stats, cannot add it")
|
||||||
:format(tostring(id)))
|
:format(tostring(id)))
|
||||||
end
|
end
|
||||||
local mon = MonOps.create(S.data, id, 5)
|
local mon = createMon(S, id, 5)
|
||||||
mon.ot = S.save.player.name
|
|
||||||
mon.otId = S.save.player.id
|
|
||||||
table.insert(box, mon)
|
table.insert(box, mon)
|
||||||
S.selectedBoxSlot = #box
|
S.selectedBoxSlot = #box
|
||||||
S.editingMon = mon
|
S.editingMon = mon
|
||||||
@@ -351,7 +425,7 @@ function Ops.setDv(S, mon, key, value)
|
|||||||
if want == mon.dvs[key] then
|
if want == mon.dvs[key] then
|
||||||
return Ops.say(S, ("%s DV is already %d"):format(key, want))
|
return Ops.say(S, ("%s DV is already %d"):format(key, want))
|
||||||
end
|
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))
|
return Ops.mark(S, ("%s DV %d (HP DV now %d)"):format(key, mon.dvs[key], mon.dvs.hp))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -382,7 +456,17 @@ end
|
|||||||
function Ops.resetMoves(S, mon)
|
function Ops.resetMoves(S, mon)
|
||||||
if not mon then return false end
|
if not mon then return false end
|
||||||
local def = S.data.pokemon[mon.species]
|
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 = {}
|
mon.moves = {}
|
||||||
for slot, id in ipairs(learned) do
|
for slot, id in ipairs(learned) do
|
||||||
MonOps.setMove(S.data, mon, slot, id)
|
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))
|
return Ops.say(S, ("%s is already at full HP"):format(mon.species))
|
||||||
end
|
end
|
||||||
mon.hp = mon.stats.hp
|
mon.hp = mon.stats.hp
|
||||||
|
if mon.maxHp then mon.maxHp = mon.stats.hp end
|
||||||
mon.status = nil
|
mon.status = nil
|
||||||
for _, mv in ipairs(mon.moves or {}) do
|
for _, mv in ipairs(mon.moves or {}) do
|
||||||
local def = S.data.moves[mv.id]
|
local def = S.data.moves[mv.id]
|
||||||
@@ -554,27 +639,35 @@ function Ops.clearNickname(S, mon)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------------ boxes
|
-- ------------------------------------------------------------------ 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)
|
function Ops.boxes(S)
|
||||||
return BoxesMod.ensure(S.save)
|
return Gen.ensureBoxes(S.save)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.selectBox(S, index)
|
function Ops.selectBox(S, index)
|
||||||
S.selectedBox = clamp(index, 1, BoxesMod.COUNT)
|
S.selectedBox = clamp(index, 1, Ops.boxCount(S))
|
||||||
S.selectedBoxSlot = 1
|
S.selectedBoxSlot = 1
|
||||||
S.save.currentBox = S.selectedBox
|
S.save.currentBox = S.selectedBox
|
||||||
local box = Ops.boxes(S)[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
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.stepBox(S, delta)
|
function Ops.stepBox(S, delta)
|
||||||
local n = BoxesMod.COUNT
|
local n = Ops.boxCount(S)
|
||||||
return Ops.selectBox(S, ((S.selectedBox - 1 + delta) % n) + 1)
|
return Ops.selectBox(S, ((S.selectedBox - 1 + delta) % n) + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.selectBoxSlot(S, index)
|
function Ops.selectBoxSlot(S, index)
|
||||||
local box = Ops.boxes(S)[S.selectedBox]
|
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]
|
local mon = box[S.selectedBoxSlot]
|
||||||
S.editingMon = mon
|
S.editingMon = mon
|
||||||
S.status = mon
|
S.status = mon
|
||||||
@@ -589,14 +682,12 @@ end
|
|||||||
-- chooses what lands in the box instead of always getting catalog entry #1.
|
-- chooses what lands in the box instead of always getting catalog entry #1.
|
||||||
function Ops.boxAdd(S)
|
function Ops.boxAdd(S)
|
||||||
local box = Ops.boxes(S)[S.selectedBox]
|
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)")
|
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||||
:format(S.selectedBox, #box, BoxesMod.CAPACITY))
|
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||||
end
|
end
|
||||||
local species = S.cat.species[1]
|
local species = S.cat.species[1]
|
||||||
local mon = MonOps.create(S.data, species, 5)
|
local mon = createMon(S, species, 5)
|
||||||
mon.ot = S.save.player.name
|
|
||||||
mon.otId = S.save.player.id
|
|
||||||
table.insert(box, mon)
|
table.insert(box, mon)
|
||||||
S.selectedBoxSlot = #box
|
S.selectedBoxSlot = #box
|
||||||
S.editingMon = mon
|
S.editingMon = mon
|
||||||
@@ -612,9 +703,16 @@ function Ops.withdraw(S)
|
|||||||
return Ops.say(S, ("Party is full (%d/%d), deposit one first")
|
return Ops.say(S, ("Party is full (%d/%d), deposit one first")
|
||||||
:format(#S.save.party, PartyMod.MAX))
|
:format(#S.save.party, PartyMod.MAX))
|
||||||
end
|
end
|
||||||
table.remove(box, S.selectedBoxSlot)
|
if Gen.ofState(S) == 2 then
|
||||||
table.insert(S.save.party, mon)
|
local Boxes2 = require("src.core.gen2.Boxes")
|
||||||
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#box, 1))
|
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
|
S.selectedParty = #S.save.party
|
||||||
return Ops.mark(S, ("Withdrew %s to party slot %d"):format(mon.species, #S.save.party))
|
return Ops.mark(S, ("Withdrew %s to party slot %d"):format(mon.species, #S.save.party))
|
||||||
end
|
end
|
||||||
@@ -639,6 +737,17 @@ function Ops.deposit(S)
|
|||||||
local i = S.selectedParty
|
local i = S.selectedParty
|
||||||
local mon = S.save.party[i]
|
local mon = S.save.party[i]
|
||||||
if not mon then return Ops.say(S, "No party slot selected") end
|
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)
|
local boxNum = BoxesMod.deposit(S.save, mon)
|
||||||
if not boxNum then
|
if not boxNum then
|
||||||
return Ops.say(S, "Every box is full, release something first")
|
return Ops.say(S, "Every box is full, release something first")
|
||||||
@@ -652,12 +761,13 @@ end
|
|||||||
|
|
||||||
-- ------------------------------------------------------------------ items
|
-- ------------------------------------------------------------------ items
|
||||||
function Ops.addMoney(S, delta)
|
function Ops.addMoney(S, delta)
|
||||||
local want = clamp((S.save.money or 0) + delta, 0, Ops.MONEY_MAX)
|
local have = Gen.money(S.save)
|
||||||
if want == S.save.money then
|
local want = clamp(have + delta, 0, Ops.MONEY_MAX)
|
||||||
|
if want == have then
|
||||||
return Ops.say(S, delta < 0 and "Money is already $0"
|
return Ops.say(S, delta < 0 and "Money is already $0"
|
||||||
or ("Money is already capped at $%d"):format(Ops.MONEY_MAX))
|
or ("Money is already capped at $%d"):format(Ops.MONEY_MAX))
|
||||||
end
|
end
|
||||||
S.save.money = want
|
Gen.setMoney(S.save, want)
|
||||||
return Ops.mark(S, ("Money set to $%d"):format(want))
|
return Ops.mark(S, ("Money set to $%d"):format(want))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -665,15 +775,29 @@ function Ops.maxMoney(S)
|
|||||||
return Ops.addMoney(S, Ops.MONEY_MAX)
|
return Ops.addMoney(S, Ops.MONEY_MAX)
|
||||||
end
|
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)
|
function Ops.addToBag(S, id)
|
||||||
if not id then return Ops.say(S, "Pick an item first") end
|
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
|
if Bag.add(S.save, id, 1, S.data) then
|
||||||
return Ops.mark(S, ("Added %s to the bag (%d/%d slots)")
|
return Ops.mark(S, ("Added %s to the bag (%d/%d %s slots)")
|
||||||
:format(id, Bag.slots(S.save), capacity))
|
:format(id, Bag.slots(S.save, S.data, pocket), capacity, pocket))
|
||||||
end
|
end
|
||||||
return Ops.say(S, ("Bag is full (%d/%d slots)")
|
return Ops.say(S, ("Bag is full (%d/%d %s slots)")
|
||||||
:format(Bag.slots(S.save), capacity))
|
:format(Bag.slots(S.save, S.data, pocket), capacity, pocket))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.bagAdjust(S, id, delta)
|
function Ops.bagAdjust(S, id, delta)
|
||||||
@@ -715,6 +839,12 @@ end
|
|||||||
function Ops.addToPc(S, id)
|
function Ops.addToPc(S, id)
|
||||||
if not id then return Ops.say(S, "Pick an item first") end
|
if not id then return Ops.say(S, "Pick an item first") end
|
||||||
local pc = Ops.pcItems(S)
|
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)
|
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]))
|
return Ops.mark(S, ("%s x%d in PC storage"):format(id, pc[id]))
|
||||||
end
|
end
|
||||||
@@ -749,27 +879,17 @@ function Ops.isBadgeId(id)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Ops.badgeIds(S)
|
function Ops.badgeIds(S)
|
||||||
local ids = {}
|
return Gen.badgeIds(S.save, S.cat)
|
||||||
for _, id in ipairs(S.cat.items) do
|
|
||||||
if Ops.isBadgeId(id) then ids[#ids + 1] = id end
|
|
||||||
end
|
|
||||||
return ids
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.toggleBadge(S, id)
|
function Ops.toggleBadge(S, id)
|
||||||
-- #515: badges are truthy inventory entries written as 1 by the in-game
|
local nowOn = Gen.toggleBadge(S.save, id)
|
||||||
-- grant (checkVictoryRewards, src/world/OverworldController.lua) and by
|
return Ops.mark(S, ("%s %s"):format(id, nowOn and "earned" or "removed"))
|
||||||
-- 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"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ----------------------------------------------------------------- events
|
-- ----------------------------------------------------------------- events
|
||||||
function Ops.setFlag(S, name, on)
|
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)))
|
return Ops.mark(S, ("%s = %s"):format(name, tostring(on and true or false)))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -801,17 +921,19 @@ end
|
|||||||
|
|
||||||
-- -------------------------------------------------------------------- dex
|
-- -------------------------------------------------------------------- dex
|
||||||
function Ops.dex(S)
|
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.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
|
return S.save.pokedex
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.dexCounts(S)
|
function Ops.dexCounts(S)
|
||||||
local dex = Ops.dex(S)
|
local dex = Ops.dex(S)
|
||||||
|
local key = Gen.dexOwnedKey(S.save)
|
||||||
local seen, owned = 0, 0
|
local seen, owned = 0, 0
|
||||||
for _ in pairs(dex.seen) do seen = seen + 1 end
|
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
|
return seen, owned, #S.cat.species
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -819,25 +941,28 @@ end
|
|||||||
-- the game's own rule, enforced here so a hand-edited dex stays legal.
|
-- the game's own rule, enforced here so a hand-edited dex stays legal.
|
||||||
function Ops.dexSeen(S, species, on)
|
function Ops.dexSeen(S, species, on)
|
||||||
local dex = Ops.dex(S)
|
local dex = Ops.dex(S)
|
||||||
|
local key = Gen.dexOwnedKey(S.save)
|
||||||
dex.seen[species] = on and true or nil
|
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"))
|
return Ops.mark(S, ("%s %s"):format(species, on and "marked seen" or "cleared"))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.dexOwned(S, species, on)
|
function Ops.dexOwned(S, species, on)
|
||||||
local dex = Ops.dex(S)
|
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
|
if on then dex.seen[species] = true end
|
||||||
return Ops.mark(S, ("%s %s"):format(species, on and "marked owned" or "un-owned"))
|
return Ops.mark(S, ("%s %s"):format(species, on and "marked owned" or "un-owned"))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.dexStamp(S)
|
function Ops.dexStamp(S)
|
||||||
local dex = Ops.dex(S)
|
local dex = Ops.dex(S)
|
||||||
|
local key = Gen.dexOwnedKey(S.save)
|
||||||
local n = 0
|
local n = 0
|
||||||
local function stamp(mon)
|
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.seen[mon.species] = true
|
||||||
dex.owned[mon.species] = true
|
dex[key][mon.species] = true
|
||||||
end
|
end
|
||||||
for _, m in ipairs(S.save.party) do stamp(m) end
|
for _, m in ipairs(S.save.party) do stamp(m) end
|
||||||
for _, box in ipairs(S.save.boxes or {}) do
|
for _, box in ipairs(S.save.boxes or {}) do
|
||||||
@@ -855,9 +980,10 @@ end
|
|||||||
|
|
||||||
function Ops.dexOwnAll(S)
|
function Ops.dexOwnAll(S)
|
||||||
local dex = Ops.dex(S)
|
local dex = Ops.dex(S)
|
||||||
|
local key = Gen.dexOwnedKey(S.save)
|
||||||
for _, species in ipairs(S.cat.species) do
|
for _, species in ipairs(S.cat.species) do
|
||||||
dex.seen[species] = true
|
dex.seen[species] = true
|
||||||
dex.owned[species] = true
|
dex[key][species] = true
|
||||||
end
|
end
|
||||||
return Ops.mark(S, ("Marked all %d species owned"):format(#S.cat.species))
|
return Ops.mark(S, ("Marked all %d species owned"):format(#S.cat.species))
|
||||||
end
|
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
|
if not Ops.arm(S, "dex-clear", "Wipe the whole Pokedex? Click again to confirm") then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
S.save.pokedex = { seen = {}, owned = {} }
|
local key = Gen.dexOwnedKey(S.save)
|
||||||
|
S.save.pokedex = { seen = {}, [key] = {} }
|
||||||
return Ops.mark(S, "Pokedex wiped")
|
return Ops.mark(S, "Pokedex wiped")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -927,6 +1054,11 @@ end
|
|||||||
-- OVERWORLD/PLATEAU tilesets, maps with connections, or fly spots the save
|
-- OVERWORLD/PLATEAU tilesets, maps with connections, or fly spots the save
|
||||||
-- has already visited.
|
-- has already visited.
|
||||||
function Ops.isOutdoor(S, map)
|
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
|
if map.def.tileset == "OVERWORLD" or map.def.tileset == "PLATEAU" then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -937,15 +1069,17 @@ end
|
|||||||
function Ops.setPlayerHere(S)
|
function Ops.setPlayerHere(S)
|
||||||
local cell = S.mapClickCell
|
local cell = S.mapClickCell
|
||||||
if not cell then return Ops.say(S, "Click a cell first") end
|
if not cell then return Ops.say(S, "Click a cell first") end
|
||||||
S.save.player.map = S.mapId
|
Gen.setPlayerHere(S.save, S.mapId, cell.cx, cell.cy)
|
||||||
S.save.player.x = cell.cx
|
|
||||||
S.save.player.y = cell.cy
|
|
||||||
return Ops.mark(S, ("Player set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
|
return Ops.mark(S, ("Player set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ops.setLastOutdoor(S, map)
|
function Ops.setLastOutdoor(S, map)
|
||||||
local cell = S.mapClickCell
|
local cell = S.mapClickCell
|
||||||
if not cell then return Ops.say(S, "Click a cell first") end
|
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
|
if not Ops.isOutdoor(S, map) then
|
||||||
return Ops.say(S, S.mapId .. " doesn't look outdoor (no connections, not visited)")
|
return Ops.say(S, S.mapId .. " doesn't look outdoor (no connections, not visited)")
|
||||||
end
|
end
|
||||||
@@ -956,8 +1090,50 @@ end
|
|||||||
function Ops.setLastHeal(S)
|
function Ops.setLastHeal(S)
|
||||||
local cell = S.mapClickCell
|
local cell = S.mapClickCell
|
||||||
if not cell then return Ops.say(S, "Click a cell first") end
|
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 }
|
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))
|
return Ops.mark(S, ("lastHeal set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
|
||||||
end
|
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
|
return Ops
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
-- in the selected box as a Lv5 mon built by the same MonOps path partyAdd
|
-- in the selected box as a Lv5 mon built by the same MonOps path partyAdd
|
||||||
-- uses, so its stats, exp and moves are consistent.
|
-- uses, so its stats, exp and moves are consistent.
|
||||||
|
|
||||||
local BoxesMod = require("src.pokemon.Boxes")
|
|
||||||
local PartyMod = require("src.pokemon.Party")
|
local PartyMod = require("src.pokemon.Party")
|
||||||
local Theme = require("Theme")
|
local Theme = require("Theme")
|
||||||
local Ops = require("Ops")
|
local Ops = require("Ops")
|
||||||
@@ -33,12 +32,12 @@ local function drawStrip(S, Kit, boxes, x, y, stripW, h)
|
|||||||
local s = Kit.scale
|
local s = Kit.scale
|
||||||
local pad = 16 * s
|
local pad = 16 * s
|
||||||
Kit.card(x, y, stripW, h)
|
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 stripTop = y + pad + Kit.textHeight("caption") + 10 * s
|
||||||
local stripInner = stripW - 2 * pad
|
local stripInner = stripW - 2 * pad
|
||||||
local bRowH = math.min(30 * s, math.max(22 * s,
|
local bRowH = math.min(30 * s, math.max(22 * s,
|
||||||
(h - (stripTop - y) - pad - (BoxesMod.COUNT - 1) * 6 * s) / BoxesMod.COUNT))
|
(h - (stripTop - y) - pad - (Ops.boxCount(S) - 1) * 6 * s) / Ops.boxCount(S)))
|
||||||
for i = 1, BoxesMod.COUNT do
|
for i = 1, Ops.boxCount(S) do
|
||||||
local ry = stripTop + (i - 1) * (bRowH + 6 * s)
|
local ry = stripTop + (i - 1) * (bRowH + 6 * s)
|
||||||
if ry + bRowH > y + h - pad then break end
|
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
|
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)
|
ry + (bRowH - Kit.textHeight("tiny")) / 2, PAL.caption)
|
||||||
local mx = x + pad + stripInner - 10 * s - countW - 8 * s - 44 * s
|
local mx = x + pad + stripInner - 10 * s - countW - 8 * s - 44 * s
|
||||||
Kit.meter(mx, ry + (bRowH - 5 * s) / 2, 44 * s, 5 * 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
|
||||||
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,
|
Kit.text("tab", ("Box %d"):format(S.selectedBox), gx,
|
||||||
y + gpad + (headH - Kit.textHeight("tab")) / 2, PAL.heading)
|
y + gpad + (headH - Kit.textHeight("tab")) / 2, PAL.heading)
|
||||||
local titleW = Kit.textWidth("tab", ("Box %d"):format(S.selectedBox))
|
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)
|
gx + titleW + 14 * s, y + gpad + (headH - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||||
local navW = 34 * s
|
local navW = 34 * s
|
||||||
if Kit.stepper(gx + ginner - 2 * navW - 8 * s, y + gpad, navW, headH, "<",
|
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
|
end
|
||||||
if Kit.button(gx + wdW + 10 * s, actY, addW, actH, addLabel,
|
if Kit.button(gx + wdW + 10 * s, actY, addW, actH, addLabel,
|
||||||
{ font = "small", radius = 9 * s,
|
{ font = "small", radius = 9 * s,
|
||||||
enabled = #box < BoxesMod.CAPACITY }) then
|
enabled = #box < Ops.boxCapacity(S) }) then
|
||||||
Ops.openBoxAddPicker(S, Kit)
|
Ops.openBoxAddPicker(S, Kit)
|
||||||
end
|
end
|
||||||
if Kit.button(gx + ginner - relW, actY, relW, actH, relLabel,
|
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.
|
-- of five slivers.
|
||||||
local cols = math.max(2, math.min(COLS,
|
local cols = math.max(2, math.min(COLS,
|
||||||
math.floor((ginner + cellGap) / (86 * s + cellGap))))
|
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)
|
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
|
-- 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
|
-- 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))
|
math.min((gridH - cellGap * (rows - 1)) / rows, 110 * s))
|
||||||
|
|
||||||
Kit.pushClip(gx, gridTop, ginner, gridH)
|
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 cc = (i - 1) % cols
|
||||||
local cr = math.floor((i - 1) / cols)
|
local cr = math.floor((i - 1) / cols)
|
||||||
local bx = gx + cc * (cellW + cellGap)
|
local bx = gx + cc * (cellW + cellGap)
|
||||||
@@ -220,7 +219,7 @@ function M.draw(S, Kit, x, y, w, h)
|
|||||||
local s = Kit.scale
|
local s = Kit.scale
|
||||||
local gap = 20 * s
|
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
|
S.save.currentBox = S.selectedBox
|
||||||
local boxes = Ops.boxes(S)
|
local boxes = Ops.boxes(S)
|
||||||
local box = boxes[S.selectedBox]
|
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 rx = cx + ci * (colW + colGap)
|
||||||
local ry = gridTop + ri * (rowH + rowGap)
|
local ry = gridTop + ri * (rowH + rowGap)
|
||||||
local isSeen = dex.seen[id] == true
|
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)
|
Theme.row(rx, ry, colW, rowH, 9 * s, 0.6)
|
||||||
local def = S.data.pokemon[id]
|
local def = S.data.pokemon[id]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
local Theme = require("Theme")
|
local Theme = require("Theme")
|
||||||
local Ops = require("Ops")
|
local Ops = require("Ops")
|
||||||
|
local Gen = require("Gen")
|
||||||
local PAL = Theme.PAL
|
local PAL = Theme.PAL
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
@@ -50,7 +51,7 @@ local function buildRows(S)
|
|||||||
if contains(name, filter) then
|
if contains(name, filter) then
|
||||||
rows[#rows + 1] = {
|
rows[#rows + 1] = {
|
||||||
label = name,
|
label = name,
|
||||||
checked = S.save.flags[name] == true,
|
checked = Gen.getFlag(S.save, name),
|
||||||
set = function(on) Ops.setFlag(S, name, on) end,
|
set = function(on) Ops.setFlag(S, name, on) end,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -107,7 +108,12 @@ function M.draw(S, Kit, x, y, w, h)
|
|||||||
-- past the card edge.
|
-- past the card edge.
|
||||||
local pillH = 32 * s
|
local pillH = 32 * s
|
||||||
local px, py = cx, y + pad
|
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
|
local pw = Kit.textWidth("small", t.label) + 32 * s
|
||||||
if px > cx and px + pw > cx + inner then
|
if px > cx and px + pw > cx + inner then
|
||||||
px = cx
|
px = cx
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
local Bag = require("src.inventory.Bag")
|
local Bag = require("src.inventory.Bag")
|
||||||
local Theme = require("Theme")
|
local Theme = require("Theme")
|
||||||
local Ops = require("Ops")
|
local Ops = require("Ops")
|
||||||
|
local Gen = require("Gen")
|
||||||
local PAL = Theme.PAL
|
local PAL = Theme.PAL
|
||||||
|
|
||||||
local M = {}
|
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)
|
local qtyW = Kit.textWidth("monoRow", qtyText)
|
||||||
Kit.textRight("monoRow", qtyText, bx - 10 * s,
|
Kit.textRight("monoRow", qtyText, bx - 10 * s,
|
||||||
y + (h - Kit.textHeight("monoRow")) / 2, PAL.heading)
|
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)
|
x + 10 * s, y + (h - Kit.textHeight("mono")) / 2, PAL.text)
|
||||||
return clicked
|
return clicked
|
||||||
end
|
end
|
||||||
@@ -55,9 +60,13 @@ end
|
|||||||
-- Each card is a function of its own rect so the wide (three column) and the
|
-- 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.
|
-- stacked (#715) layouts are the same drawing code with different geometry.
|
||||||
|
|
||||||
local function moneyHeight(Kit, s, pad)
|
local function moneyHeight(Kit, s, pad, S)
|
||||||
return pad * 2 + Kit.textHeight("caption") + 8 * s
|
local h = pad * 2 + Kit.textHeight("caption") + 8 * s
|
||||||
+ Kit.textHeight("headline") + 10 * s + 30 * 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
|
end
|
||||||
|
|
||||||
local function drawMoney(S, Kit, x, y, w, h)
|
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
|
local maxW = 74 * s
|
||||||
if Kit.button(x + w - pad - maxW, y + pad - 4 * s, maxW, 26 * s, "Max out",
|
if Kit.button(x + w - pad - maxW, y + pad - 4 * s, maxW, 26 * s, "Max out",
|
||||||
{ kind = "accent", font = "tiny", radius = 7 * s,
|
{ 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)
|
Ops.maxMoney(S)
|
||||||
end
|
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)
|
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 mbY = y + h - pad - 30 * s
|
||||||
local mbW = (w - 2 * pad - 3 * 8 * s) / 4
|
local mbW = (w - 2 * pad - 3 * 8 * s) / 4
|
||||||
for i, delta in ipairs(MONEY_STEPS) do
|
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)
|
Kit.card(x, y, w, h)
|
||||||
local earned = 0
|
local earned = 0
|
||||||
for _, id in ipairs(badgeIds) do
|
for _, id in ipairs(badgeIds) do
|
||||||
-- #515: truthy check, not `== true` -- the in-game grant path stores a
|
if Gen.hasBadge(S.save, id) then earned = earned + 1 end
|
||||||
-- number (see OverworldController.lua checkVictoryRewards), matching
|
|
||||||
-- src/inventory/Badges.lua's own truthy read.
|
|
||||||
if S.save.inventory[id] then earned = earned + 1 end
|
|
||||||
end
|
end
|
||||||
Kit.caption(x + pad, y + pad, "BADGES")
|
Kit.caption(x + pad, y + pad, "BADGES")
|
||||||
Kit.textRight("mono", ("%d/%d"):format(earned, #badgeIds), x + w - pad,
|
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
|
for i, id in ipairs(badgeIds) do
|
||||||
local bc = (i - 1) % BADGE_COLS
|
local bc = (i - 1) % BADGE_COLS
|
||||||
local br = math.floor((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$", "")
|
local short = id:gsub("BADGE$", "")
|
||||||
if Kit.chip(x + pad + bc * (bW + 7 * s), bTop + br * (28 * s + 7 * s),
|
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,
|
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.
|
-- drag over their own bodies first.
|
||||||
local off = Theme.clamp(S.itemsScroll or 0, 0,
|
local off = Theme.clamp(S.itemsScroll or 0, 0,
|
||||||
math.max(0, (S._itemsContentH or 0) - h))
|
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 badgeH = badgeHeight(S, Kit, s, pad)
|
||||||
local pickH = 280 * s
|
local pickH = 280 * s
|
||||||
local listH = 300 * 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
|
-- 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
|
-- over: cycling through ~250 item ids in a two-row list was the thing that
|
||||||
-- made the old panel unusable.
|
-- 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)
|
local badgeH = badgeHeight(S, Kit, s, pad)
|
||||||
drawMoney(S, Kit, x, y, leftW, moneyH)
|
drawMoney(S, Kit, x, y, leftW, moneyH)
|
||||||
drawPicker(S, Kit, x, y + moneyH + gap, leftW, h - moneyH - badgeH - 2 * gap)
|
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 Warp = require("src.world.Warp")
|
||||||
local Theme = require("Theme")
|
local Theme = require("Theme")
|
||||||
local Ops = require("Ops")
|
local Ops = require("Ops")
|
||||||
|
local Gen = require("Gen")
|
||||||
local PAL = Theme.PAL
|
local PAL = Theme.PAL
|
||||||
|
|
||||||
local MapBrowser = {}
|
local MapBrowser = {}
|
||||||
|
|
||||||
local CELL = 16 -- the walk grid; a cell is 16px of map art
|
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)
|
local function clampZoom(z)
|
||||||
if z < 1 then return 1 end
|
if z < 1 then return 1 end
|
||||||
if z > 4 then return 4 end
|
if z > 4 then return 4 end
|
||||||
@@ -38,7 +44,7 @@ MapBrowser.centerOn = centerOn
|
|||||||
|
|
||||||
local function sortedMapIds(data)
|
local function sortedMapIds(data)
|
||||||
local ids = {}
|
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)
|
table.sort(ids)
|
||||||
return ids
|
return ids
|
||||||
end
|
end
|
||||||
@@ -52,7 +58,19 @@ local OUTSIDE_TILESETS = { OVERWORLD = true, PLATEAU = true }
|
|||||||
|
|
||||||
local function goToWarp(S, warp)
|
local function goToWarp(S, warp)
|
||||||
local def = warp.def
|
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]
|
if fromMap and OUTSIDE_TILESETS[fromMap.tileset]
|
||||||
and def.destMap ~= "LAST_MAP" and def.destMap ~= S.mapId then
|
and def.destMap ~= "LAST_MAP" and def.destMap ~= S.mapId then
|
||||||
S.save.lastOutdoor = { id = S.mapId, x = def.x, y = def.y }
|
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
|
return cx * CELL - S.mapCamX, cy * CELL - S.mapCamY, CELL, CELL
|
||||||
end
|
end
|
||||||
love.graphics.setColor(0.27, 0.59, 1, 0.55)
|
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))
|
love.graphics.rectangle("line", cellRect(wdef.x, wdef.y))
|
||||||
end
|
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.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
|
end
|
||||||
local heal = S.save.lastHeal
|
if Gen.of(S.save) ~= 2 then
|
||||||
if heal and heal.map == S.mapId then
|
local heal = S.save.lastHeal
|
||||||
love.graphics.setColor(0.24, 0.88, 0.54, 0.9)
|
if heal and heal.map == S.mapId then
|
||||||
love.graphics.rectangle("line", cellRect(heal.x, heal.y))
|
love.graphics.setColor(0.24, 0.88, 0.54, 0.9)
|
||||||
end
|
love.graphics.rectangle("line", cellRect(heal.x, heal.y))
|
||||||
local out = S.save.lastOutdoor
|
end
|
||||||
if out and out.id == S.mapId then
|
local out = S.save.lastOutdoor
|
||||||
love.graphics.setColor(1, 0.8, 0.02, 0.9)
|
if out and out.id == S.mapId then
|
||||||
love.graphics.rectangle("line", cellRect(out.x, out.y))
|
love.graphics.setColor(1, 0.8, 0.02, 0.9)
|
||||||
|
love.graphics.rectangle("line", cellRect(out.x, out.y))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if S.mapClickCell then
|
if S.mapClickCell then
|
||||||
love.graphics.setColor(1, 1, 0.35, 0.95)
|
love.graphics.setColor(1, 1, 0.35, 0.95)
|
||||||
@@ -239,9 +260,13 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
|||||||
#ids, perPage)
|
#ids, perPage)
|
||||||
if Kit.button(lr.x + pad, gotoY, listInner, gotoH, "Go to save location",
|
if Kit.button(lr.x + pad, gotoY, listInner, gotoH, "Go to save location",
|
||||||
{ font = "small", radius = 9 * s }) then
|
{ font = "small", radius = 9 * s }) then
|
||||||
MapBrowser.select(S, S.save.player.map)
|
local pmap, px, py = playerPos(S)
|
||||||
Ops.say(S, ("Jumped to %s (%d,%d)"):format(S.save.player.map,
|
if pmap then
|
||||||
S.save.player.x, S.save.player.y))
|
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
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------------------- the viewport
|
-- ---------------------------------------------------------- the viewport
|
||||||
@@ -253,7 +278,32 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
|||||||
Kit.text("monoBig", tostring(S.mapId), vx0,
|
Kit.text("monoBig", tostring(S.mapId), vx0,
|
||||||
vr.y + vpad + (headH - Kit.textHeight("monoBig")) / 2, PAL.heading)
|
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
|
if not ok then
|
||||||
Kit.text("mono", "Failed to load map: " .. tostring(map), vx0,
|
Kit.text("mono", "Failed to load map: " .. tostring(map), vx0,
|
||||||
vr.y + vpad + headH + 20 * s, PAL.red)
|
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 zBtn = 32 * s
|
||||||
local rightEdge = vx0 + vinner
|
local rightEdge = vx0 + vinner
|
||||||
local zoomW = 2 * zBtn + 56 * s + 12 * s
|
local zoomW = 2 * zBtn + 56 * s + 12 * s
|
||||||
|
local pmap, px, py = playerPos(S)
|
||||||
local showCenter = vinner >= zoomW + 10 * s + centerW + 160 * s
|
local showCenter = vinner >= zoomW + 10 * s + centerW + 160 * s
|
||||||
if showCenter then
|
if showCenter then
|
||||||
if Kit.button(rightEdge - centerW, vr.y + vpad, centerW, headH, "Center on player",
|
if Kit.button(rightEdge - centerW, vr.y + vpad, centerW, headH, "Center on player",
|
||||||
{ kind = "accent", font = "small", radius = 7 * s }) then
|
{ kind = "accent", font = "small", radius = 7 * s }) then
|
||||||
if S.save.player.map == S.mapId then
|
if pmap == S.mapId then
|
||||||
centerOn(S, S.save.player.x, S.save.player.y)
|
centerOn(S, px, py)
|
||||||
Ops.say(S, "Centred on the player")
|
Ops.say(S, "Centred on the player")
|
||||||
else
|
else
|
||||||
Ops.say(S, "Player isn't on this map")
|
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.
|
-- the panel has laid itself out.
|
||||||
if S._mapCenteredFor ~= S.mapId then
|
if S._mapCenteredFor ~= S.mapId then
|
||||||
S._mapCenteredFor = S.mapId
|
S._mapCenteredFor = S.mapId
|
||||||
if S.save.player.map == S.mapId then
|
if pmap == S.mapId then
|
||||||
centerOn(S, S.save.player.x, S.save.player.y)
|
centerOn(S, px, py)
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -334,7 +386,24 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
|
|||||||
love.graphics.push()
|
love.graphics.push()
|
||||||
love.graphics.translate(vx0, vy0)
|
love.graphics.translate(vx0, vy0)
|
||||||
love.graphics.scale(S.mapZoom, S.mapZoom)
|
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)
|
drawOverlays(S, map)
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
love.graphics.setScissor()
|
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")
|
Kit.caption(sx0 + pad, sr.y + pad, "SPAWN POINTS")
|
||||||
local sTop = sr.y + pad + Kit.textHeight("caption") + 12 * s
|
local sTop = sr.y + pad + Kit.textHeight("caption") + 12 * s
|
||||||
local sInner = sr.w - 2 * pad
|
local sInner = sr.w - 2 * pad
|
||||||
local player = S.save.player
|
local pmap2, px2, py2 = playerPos(S)
|
||||||
local out = S.save.lastOutdoor
|
local playerValue = pmap2 and ("%s (%d,%d)"):format(pmap2, px2, py2) or "unset"
|
||||||
local heal = S.save.lastHeal
|
local spawns
|
||||||
local spawns = {
|
if Gen.of(S.save) == 2 then
|
||||||
{ key = "PLAYER", color = PAL.red,
|
spawns = {
|
||||||
value = ("%s (%d,%d)"):format(player.map, player.x, player.y),
|
{ key = "PLAYER", color = PAL.red, value = playerValue,
|
||||||
set = function() Ops.setPlayerHere(S) end },
|
set = function() Ops.setPlayerHere(S) end },
|
||||||
{ key = "LAST HEAL", color = PAL.green,
|
{ key = "SPAWN", color = PAL.green,
|
||||||
value = heal and ("%s (%d,%d)"):format(heal.map, heal.x, heal.y) or "unset",
|
value = tostring(S.save.spawn or "SPAWN_HOME"),
|
||||||
set = function() Ops.setLastHeal(S) end },
|
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",
|
else
|
||||||
set = function() Ops.setLastOutdoor(S, map) end },
|
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
|
local spawnH = 62 * s
|
||||||
for i, sp in ipairs(spawns) do
|
for i, sp in ipairs(spawns) do
|
||||||
local ry = sTop + (i - 1) * (spawnH + 8 * s)
|
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)
|
sx0 + pad + 12 * s, ry + spawnH - 10 * s - Kit.textHeight("mono"), PAL.muted)
|
||||||
end
|
end
|
||||||
|
|
||||||
local noteY = sTop + 3 * (spawnH + 8 * s) + 6 * s
|
local noteY = sTop + #spawns * (spawnH + 8 * s) + 6 * s
|
||||||
Kit.textCenter("tiny",
|
Kit.textCenter("tiny",
|
||||||
"Click a cell first. Warp cells follow the warp instead of selecting. " ..
|
"Click a cell first. Warp cells follow the warp instead of selecting. " ..
|
||||||
"Arrow keys / WASD pan, the wheel zooms.",
|
"Arrow keys / WASD pan, the wheel zooms.",
|
||||||
|
|||||||
@@ -19,17 +19,26 @@
|
|||||||
local Theme = require("Theme")
|
local Theme = require("Theme")
|
||||||
local Ops = require("Ops")
|
local Ops = require("Ops")
|
||||||
local PAL = Theme.PAL
|
local PAL = Theme.PAL
|
||||||
|
local Gen = require("Gen")
|
||||||
|
|
||||||
local MonEditor = {}
|
local MonEditor = {}
|
||||||
|
|
||||||
local DV_KEYS = { "attack", "defense", "speed", "special" }
|
local DV_KEYS = { "attack", "defense", "speed", "special" }
|
||||||
local STAT_KEYS = {
|
local STAT_KEYS_G1 = {
|
||||||
{ key = "HP", field = "hp" },
|
{ key = "HP", field = "hp" },
|
||||||
{ key = "ATK", field = "attack" },
|
{ key = "ATK", field = "attack" },
|
||||||
{ key = "DEF", field = "defense" },
|
{ key = "DEF", field = "defense" },
|
||||||
{ key = "SPD", field = "speed" },
|
{ key = "SPD", field = "speed" },
|
||||||
{ key = "SPC", field = "special" },
|
{ 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
|
-- Front sprites are read straight off the generated cache. One image per
|
||||||
-- species, cached for the process: the old panel called newImage every frame,
|
-- 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
|
end
|
||||||
lx = lx + bw + 8 * s
|
lx = lx + bw + 8 * s
|
||||||
end
|
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)
|
ly + (lh - Kit.textHeight("mono")) / 2, PAL.muted)
|
||||||
return lh
|
return lh
|
||||||
end
|
end
|
||||||
@@ -106,7 +115,7 @@ end
|
|||||||
local function levelRowWidth(Kit, mon)
|
local function levelRowWidth(Kit, mon)
|
||||||
local s = Kit.scale
|
local s = Kit.scale
|
||||||
return 52 * s + 2 * (40 * s + 8 * s) + 58 * s + 8 * s + 2 * (40 * s + 8 * s)
|
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
|
end
|
||||||
|
|
||||||
local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
|
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)
|
local tw = math.min(w - 40 * s, 340 * s)
|
||||||
Kit.textCenter("button",
|
Kit.textCenter("button",
|
||||||
"Pick a slot on the left to inspect it. Every change here re-runs the " ..
|
"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)
|
x + (w - tw) / 2, y + h / 2 - Kit.textHeight("button"), tw, PAL.muted)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -220,10 +229,13 @@ function MonEditor.draw(S, Kit, x, y, w, h)
|
|||||||
end
|
end
|
||||||
-- the nickname section: a caption line (with the Clear button on it) plus
|
-- the nickname section: a caption line (with the Clear button on it) plus
|
||||||
-- the field + Set row
|
-- the field + Set row
|
||||||
|
local extraH = 0
|
||||||
|
if Gen.ofState(S) == 2 then extraH = 88 * s end
|
||||||
local nickFieldH = 30 * s
|
local nickFieldH = 30 * s
|
||||||
local contentH = pad + headerH + 18 * s
|
local contentH = pad + headerH + 18 * s
|
||||||
+ capH + 10 * s + nickFieldH + 18 * s
|
+ capH + 10 * s + nickFieldH + 18 * s
|
||||||
+ capH + 10 * s + cellH + 18 * s
|
+ capH + 10 * s + cellH + 18 * s
|
||||||
|
+ extraH
|
||||||
+ colsH + pad
|
+ colsH + pad
|
||||||
|
|
||||||
-- Called before the widgets so this frame already draws at the updated
|
-- 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
|
local statsY = nickY + capH + 10 * s + nickFieldH + 18 * s
|
||||||
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
|
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
|
||||||
statsY = statsY + capH + 10 * s
|
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 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
|
for i, st in ipairs(STAT_KEYS) do
|
||||||
local bx = cx + (i - 1) * (cellW + gap)
|
local bx = cx + (i - 1) * (cellW + gap)
|
||||||
Theme.row(bx, statsY, cellW, cellH, 10 * s, 0.6)
|
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
|
-- --------------------------------------------------- DVs | moves split
|
||||||
local colY = statsY + cellH + 18 * s
|
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
|
if narrow then
|
||||||
-- stacked: DVs first, then moves, then the two actions side by side at
|
-- stacked: DVs first, then moves, then the two actions side by side at
|
||||||
-- full width (#715)
|
-- full width (#715)
|
||||||
|
|||||||
Reference in New Issue
Block a user