followups

This commit is contained in:
bryanthaboi
2026-10-07 09:14:37 -04:00
parent fcdaecd496
commit cafb6e63da
78 changed files with 4675 additions and 454 deletions
+3 -1
View File
@@ -45,7 +45,9 @@ untouched):
reports itself up to date (it never chases a release, and it never counts
as a valid payload to chainload).
- `shell` - the native-shell contract this build's fused executable
implements.
implements. Any change to `main.lua`'s `love.run` bumps it and adds the new
loop as `tests/data/love_run/shell<N>.lua`; `tests/engine/love_run_shell_matrix.lua`
fails until both are done, and runs the payload under every shipped loop.
- `payloadHost` - the native host family an in-place payload targets. Ordinary
LÖVE packages use `"love"`. A specialized native package uses a distinct,
stable identifier and accepts only payloads carrying that same identifier.
+1 -1
View File
@@ -7,7 +7,7 @@ local Version = {
engine = "0.0.0-dev", -- game/engine release (semver). Repo default is the
-- "-dev" placeholder; CI stamps the real X.Y.Z into
-- the packed game.love only, never the working tree.
shell = 2, -- native-shell contract this build implements
shell = 3, -- native-shell contract this build implements
payloadHost = "love", -- native host family for in-place Lua payloads.
-- A payload must name the same family; this prevents
-- mounting code packaged for a different native host.
+1 -1
View File
@@ -3820,7 +3820,7 @@ AnimTasks.REGISTRY.AnimTask_ExtrasensoryDistortion = AnimTasks.ScreenDistortionW
AnimTasks.REGISTRY.UproarDistortion = AnimTasks.ScreenDistortionWobble
AnimTasks.REGISTRY.AnimTask_UproarDistortion = AnimTasks.ScreenDistortionWobble
--- pret CreateSpotlight & RemoveSpotlight (Spotlight, Follow Me)
-- pokeemerald/src/battle_anim_effects_3.c:1674
function AnimTasks.Spotlight(t, vm)
local frame = t.data[14] or 0
t.data[14] = frame + 1
+7 -10
View File
@@ -2193,13 +2193,8 @@ local function battle_font()
return require(P.font.module)
end
local function is_rs_battle()
local P = require("src.core.game3.profile").forSession(Ui._session)
return P.font.nativeLayout == "rs"
end
function Ui.drawMenuFrames(panelMode)
BattleChrome.drawMenuFrames(panelMode, is_rs_battle() and "rs" or nil)
BattleChrome.drawMenuFrames(panelMode)
end
local function c5to8(x)
@@ -2383,8 +2378,9 @@ local function draw_move_menu_rse(st)
end
local function draw_action_menu(st)
if is_rs_battle() then return draw_action_menu_rs(st) end
if BattleChrome.isRse() then return draw_action_menu_rse(st) end
local L = BattleChrome.layout()
if L == "rs" then return draw_action_menu_rs(st) end
if L == "emerald" then return draw_action_menu_rse(st) end
-- B_WIN_ACTION_PROMPT @ (1,15) after scroll → px (8,120); printer (2,2) → (10,122)
-- B_WIN_ACTION_MENU @ (17,15) → (136,120); printer (0,2) → (136,122)
-- ActionSelectionCreateCursorAt: tile (16+7*col, 35+row) → after scroll (128,120);
@@ -2409,8 +2405,9 @@ local function draw_action_menu(st)
end
local function draw_move_menu(st)
if is_rs_battle() then return draw_move_menu_rs(st) end
if BattleChrome.isRse() then return draw_move_menu_rse(st) end
local L = BattleChrome.layout()
if L == "rs" then return draw_move_menu_rs(st) end
if L == "emerald" then return draw_move_menu_rse(st) end
local ab = st and (is_double(st) and active_battler(st) or st.player)
local mon = ab and ab.mon
local positions = {
+4 -1
View File
@@ -145,7 +145,10 @@ return {
},
startMenu = "src.ui.game3.rs.start_menu_data",
shopMenu = "src.ui.game3.rs.shop_menu",
saveMenu = "rse",
-- pokeruby/src/save_menu_util.c:12
saveMenu = "rs",
-- pokeruby/src/coins.c:16
coinsWindow = { frameOrigin = true, text = "gOtherText_Coins2" },
battleSpriteLayout = "rs",
healthbox = { layout = "rs", doublesCenters = {
[0] = { x = 159, y = 77 }, [1] = { x = 44, y = 19 },
+12 -5
View File
@@ -3,7 +3,7 @@
-- 1. Lockstep Event Queue: Prevents simultaneous tick collisions; flushes on party white-out.
-- 2. Happiness Step Counter (128 steps): +1 friendship to all party Pokémon.
-- 3. VS Seeker Battery (100 steps): Increments while the VS SEEKER is in the bag.
-- 4. Overworld Poison (4 steps): 4-frame reddish screen flash, SE_FIELD_POISON, lethal faint at 0 HP.
-- 4. Overworld Poison (4 steps RSE, 5 FRLG): 4-frame reddish screen flash, SE_FIELD_POISON, lethal faint at 0 HP.
-- 5. Egg Cycles & Daycare (daycare stepCounter == 255): Decrements egg cycles -> EggHatch; +1 EXP per step in Daycare.
-- 6. Repel Counter: Decrements steps -> Text_RepelWoreOff on expiration.
@@ -231,11 +231,18 @@ function StepEvents.onStepTaken(session, game)
return
end
-- 3. Overworld Poison Counter (every 4 steps, pret field_poison.c)
-- pokeemerald/src/field_control_avatar.c:641
local Map = package.loaded["src.core.game3.map"]
local curDef = Map and Map.currentDef and Map.currentDef()
-- pokeemerald/include/constants/map_types.h:13
local psnGate = not forced and not (curDef and tonumber(curDef.mapType) == 9)
local psnVar = Sem.var(session, "poisonSteps")
local psnSteps = (tonumber(session.vars[psnVar] or session.poisonSteps) or 0) + 1
if psnSteps >= 4 then
psnSteps = 0
local psnSteps = tonumber(session.vars[psnVar] or session.poisonSteps) or 0
-- pokeemerald/src/field_control_avatar.c:645
-- pokefirered/src/field_control_avatar.c:718
local psnPeriod = isRse and 4 or 5
if psnGate then psnSteps = (psnSteps + 1) % psnPeriod end
if psnGate and psnSteps == 0 then
local anyPoisonDamage = false
local faintedMons = {}
local profileField = require("src.core.game3.profile").forSession(session).field
+26 -34
View File
@@ -3,6 +3,7 @@ local Theme = require("src.ui.kit.Theme")
local Transition = require("src.ui.kit.Transition")
local Strings = require("src.core.Strings")
local Icons = require("src.ui.kit.Icons")
local Toast = require("src.ui.kit.Toast")
local UI = {}
local PAL = Theme.PAL
UI.ICONS = { List = "book-open", Filters = "list-filter", Arrange = "arrow-left-right",
@@ -31,46 +32,37 @@ function UI.button(imp, x, y, w, h, id, label, action, opts)
opts = opts or {}; opts.action, opts.font = action, opts.font or "small"
view().btn(imp, x, y, w, h, "box-" .. id, Strings(label), opts)
end
local TOAST = { ok = { color = "green", icon = "check", hold = 3.5 },
error = { color = "red", icon = "triangle-alert", hold = 7 },
info = { color = "blue", icon = "circle-help", hold = 5 } }
function UI.toast(imp, text, kind, sticky)
if text == nil or text == "" then imp._boxToast = nil; return end
imp._boxToast = { text = Strings(tostring(text)), kind = TOAST[kind] and kind or "info",
at = love.timer.getTime(), sticky = sticky }
local function toastStore(imp, key)
imp._toasts = imp._toasts or {}
imp._toasts[key] = imp._toasts[key] or {}
return imp._toasts[key]
end
function UI.toast(imp, text, kind, sticky, key)
local store = toastStore(imp, key or "box")
if text == nil or text == "" then return Toast.clear(store) end
Toast.show(store, Strings(tostring(text)), kind, { sticky = sticky })
end
function UI.currentToast(imp)
local store = imp._toasts and imp.tab and imp._toasts[imp.tab]
return store and Toast.current(store) and store or nil
end
function UI.occludeToast(imp)
local t = imp.tab == "box" and imp._boxToast
if t and t.rect then Kit.occlude(t.rect[1], t.rect[2], t.rect[3], t.rect[4]) end
local store = UI.currentToast(imp)
local r = store and store.toast.rect
if r then Kit.occlude(r[1], r[2], r[3], r[4]) end
end
local function wrapHeight(text, w, maxLines) return Kit.wrapHeight("small", text, w, maxLines) end
local function drawText(text, x, y, w, c, maxLines) Kit.textWrapped("small", text, x, y, w, c, maxLines) end
function UI.drawToast(imp, m, bottom)
local t = imp._boxToast
if not t or imp.tab ~= "box" then return end
local style = TOAST[t.kind]
local elapsed = love.timer.getTime() - t.at
local hold = t.sticky and math.huge or style.hold
if elapsed > hold + .25 then imp._boxToast = nil; return end
local slide = math.min(1, elapsed / .18, (hold + .25 - elapsed) / .25)
slide = 1 - (1 - math.max(0, slide)) ^ 3
local s, color = m.s, PAL[style.color]
local pad, gap, icon = 12 * s, 10 * s, 20 * s
local w = math.min(420 * s, m.W - 32 * s)
local textW = w - 2 * pad - icon - gap
local h = 2 * pad + math.max(icon, Kit.wrapHeight("small", t.text, textW, 4))
local x = (m.W - w) / 2
local y = bottom - h - 12 * s + (1 - slide) * (h + 24 * s)
t.rect = { x, y, w, h }
local r = Theme.cardRadius()
Theme.fillRounded(x + 2 * s, y + 4 * s, w, h, PAL.inverse, .35, r)
Theme.fillRounded(x, y, w, h, PAL.surface, .98, r)
Theme.fillRounded(x, y, w, h, color, .16, r)
Theme.strokeRounded(x, y, w, h, color, .75, 1.5, r)
Icons.draw(style.icon, x + pad, y + (h - icon) / 2, icon, color, 1)
Kit.textWrapped("small", t.text, x + pad + icon + gap, y + pad, textW, PAL.heading, 4)
local store = UI.currentToast(imp)
if not store then return end
local rect = Toast.draw(store, { x = 0, w = m.W, bottom = bottom, s = m.s,
wrapHeight = wrapHeight, drawText = drawText })
if not rect then return end
local overlay = Kit._overlay
Kit._overlay = true
view().btn(imp, x, y, w, h, "box-toast", "", { face = "bare", ring = false,
action = function() imp._boxToast = nil end })
view().btn(imp, rect[1], rect[2], rect[3], rect[4], "box-toast", "", { face = "bare", ring = false,
action = function() Toast.clear(store) end })
Kit._overlay = overlay
end
function UI.close(imp)
+15 -1
View File
@@ -647,6 +647,7 @@ function RomImporter:_setNxSavesInboxNotice(version)
self.saveNotice = self.saveNotice or {}
self.saveNotice[version] = {
ok = true,
persistent = true,
text = Strings("Copy your %s .sav into:\n%s/%s/\nDBI MTP → 1: SD Card/%s%s/",
game, saveDir, inbox, rel, inbox),
}
@@ -959,6 +960,7 @@ function RomImporter:rescanSavesAction(version)
elseif skipCount > 0 then
self.saveNotice[version] = {
ok = true,
kind = "info",
text = Strings("Already imported: %d file(s) skipped. Check SAVE SLOT.",
skipCount),
}
@@ -3409,6 +3411,7 @@ function RomImporter:exportSave(version, format, scope, slotId)
local outDir = exportsDir(version)
self.saveNotice[noticeScope] = {
ok = true,
persistent = true,
text = Strings("Exported to %s\nDBI MTP → 1: SD Card/%s%s/", res, rel, outDir),
}
return
@@ -3433,7 +3436,7 @@ function RomImporter:exportSave(version, format, scope, slotId)
if love.system.createFile and love.system.createFile(suggested, love.filesystem.getSaveDirectory()) then
self.pickPending = true
self.pickTimer = 0
self.saveNotice[noticeScope] = { ok = true,
self.saveNotice[noticeScope] = { ok = true, kind = "info",
text = "Pick where to save " .. suggested .. "..." }
else
self.androidPendingExportVersion = nil
@@ -4647,7 +4650,18 @@ end
-- Pair with POKEPORT_LAUNCHER_TAB / POKEPORT_WIN to profile a specific panel.
local profN, profSamples = tonumber(os.getenv("POKEPORT_LAUNCHER_PROF") or ""), {}
function RomImporter:toastSaveNotices()
local BoxUI = require("src.import.BoxUI")
for scope, notice in pairs(self.saveNotice or {}) do
if not notice.persistent and notice.text and notice.text ~= "" then
BoxUI.toast(self, notice.text, notice.kind or (notice.ok and "ok" or "error"), nil, self.tab)
self.saveNotice[scope] = notice.dir and { ok = notice.ok, text = "", dir = notice.dir } or nil
end
end
end
function RomImporter:draw()
self:toastSaveNotices()
local View = require("src.import.LauncherView")
if not profN then return View.draw(self) end
local t0 = love.timer.getTime()
+13 -9
View File
@@ -413,11 +413,14 @@ end
function BattleChrome.layout()
local m = BattleChrome._manifest
return type(m) == "table" and m.layout or "frlg"
if type(m) ~= "table" then return "frlg" end
if m.assetLayout == "rs" or m.layout == "rs" then return "rs" end
return m.layout == "rse" and "emerald" or "frlg"
end
function BattleChrome.isRse()
return BattleChrome.layout() == "rse"
function BattleChrome.isHoenn()
local l = BattleChrome.layout()
return l == "rs" or l == "emerald"
end
-- pokeemerald/include/constants/battle.h:347
@@ -463,7 +466,7 @@ function BattleChrome.textOrigin(id)
end
function BattleChrome.messageOrigin()
if not BattleChrome.isRse() then return 10, 122, 224 end
if BattleChrome.layout() ~= "emerald" then return 10, 122, 224 end
local x, y, w = BattleChrome.textOrigin(BattleChrome.WIN.MSG)
return x, y, w
end
@@ -500,15 +503,16 @@ BattleChrome.RS_MENU_FRAMES = {
}
function BattleChrome.menuFrameRects(mode, layout)
local set = layout == "rs" and BattleChrome.RS_MENU_FRAMES or BattleChrome.RSE_MENU_FRAMES
return set[mode]
local set = (layout == "rs" and BattleChrome.RS_MENU_FRAMES)
or (layout == "emerald" and BattleChrome.RSE_MENU_FRAMES) or nil
return set and set[mode]
end
-- pokeemerald/src/battle_bg.c:744
-- pokeruby/src/battle_bg.c:266
function BattleChrome.drawMenuFrames(mode, layout)
if not BattleChrome.isRse() then return end
local rects = BattleChrome.menuFrameRects(mode, layout)
function BattleChrome.drawMenuFrames(mode)
if not BattleChrome.isHoenn() then return end
local rects = BattleChrome.menuFrameRects(mode, BattleChrome.layout())
if not rects then return end
local Chrome = require("src.ui.game3.chrome")
for _, r in ipairs(rects) do
+152 -12
View File
@@ -36,6 +36,25 @@ function Choice.reset()
return true
end
local function uiProfile()
local okR, Runtime = pcall(require, "src.core.game3.runtime")
local session = okR and type(Runtime) == "table" and Runtime.getSession and Runtime.getSession() or nil
local okP, Profile = pcall(require, "src.core.game3.profile")
local okF, row = pcall(function() return okP and Profile.forSession(session) end)
return okF and type(row) == "table" and type(row.ui) == "table" and row.ui or nil
end
function Choice.fieldLayout()
local ui = uiProfile()
return ui and ui.saveMenu == "rs" and "rs" or "frlg"
end
-- pokeemerald/src/menu.c:98, pokefirered/src/new_menu_helpers.c:48
function Choice.yesNoWidth()
local ui = uiProfile()
return ui and ui.saveMenu == "rse" and 5 or 6
end
function Choice.yesNo(cb, layout)
Choice.active = true
Choice.kind = "yesno"
@@ -51,9 +70,18 @@ function Choice.yesNo(cb, layout)
end
Choice.cursor = 1
Choice.done = cb
-- pokefirered/src/new_menu_helpers.c:48
Choice.left = tonumber(layout.left) or 21
Choice.top = tonumber(layout.top) or 9
if layout.style == "battle" then
Choice.left = tonumber(layout.left) or 24
Choice.top = tonumber(layout.top) or 9
elseif Choice.fieldLayout() == "rs" then
-- pokeruby/src/script_menu.c:765, pokeruby/src/start_menu.c:698
Choice.left = tonumber(layout.left) or 20
Choice.top = tonumber(layout.top) or 8
else
-- pokeemerald/src/script_menu.c:198, pokefirered/src/script_menu.c:864
Choice.left = 21
Choice.top = 9
end
Choice.maxRight = nil
Choice.cols = 1
Choice.ignoreBPress = layout.ignoreBPress or false
@@ -168,7 +196,18 @@ end
-- pokefirered/src/menu.c:531
function Choice.drawYesNo(L, Tp, cursor, labels)
Window.stdFrame(Window.template(L, Tp, 6, 4))
if Choice.fieldLayout() == "rs" then
-- pokeruby/src/menu.c:608
Window.stdFrame(Window.template(L + 1, Tp + 1, 5, 4))
for i, lab in ipairs(labels) do
-- pokeruby/src/menu.c:602
Window.printPx(lab, (L + 1) * 8, (Tp + 1 + 2 * (i - 1)) * 8)
end
-- pokeruby/src/menu.c:721, :750
require("src.ui.game3.rs.menu_cursor").draw((L + 1) * 8, (Tp + 1) * 8 + (cursor - 1) * 16, 40)
return
end
Window.stdFrame(Window.template(L, Tp, Choice.yesNoWidth(), 4))
for i, lab in ipairs(labels) do
local rowPx = Tp * 8 + 2 + (i - 1) * 14
if i == cursor then Window.cursorPx(L * 8, rowPx) end
@@ -176,22 +215,118 @@ function Choice.drawYesNo(L, Tp, cursor, labels)
end
end
function Choice.battleYesNoGeometry(layout, L, Tp)
L, Tp = L or 24, Tp or 9
if layout == "rs" then
-- pokeruby/src/battle_script_commands.c:5273
return {
frame = { left = L + 1, top = Tp, width = 4, height = 4 },
cursor = "rs", cursorX = (L + 1) * 8, cursorWidth = 32,
-- pokeruby/src/battle_script_commands.c:9617
rowY = function(i) return Tp * 8 + (i - 1) * 16 end,
-- pokeruby/src/battle_script_commands.c:5274
textX = (L + 1) * 8, textDy = 0,
}
elseif layout == "emerald" then
-- pokeemerald/include/battle_script_commands.h:11
return {
frame = { left = L + 1, top = Tp, width = 4, height = 4 },
-- pokeemerald/src/battle_script_commands.c:10206
cursor = "arrow", cursorX = (L + 1) * 8,
rowY = function(i) return (Tp + (i - 1) * 2) * 8 end,
-- pokeemerald/src/battle_message.c:1600
textX = (L + 2) * 8, textDy = 1,
}
end
-- pokefirered/src/battle_script_commands.c:5149
return {
frame = { left = L, top = Tp, width = 5, height = 4 },
-- pokefirered/src/battle_script_commands.c:9775
cursor = "arrow", cursorX = L * 8,
rowY = function(i) return (Tp + (i - 1) * 2) * 8 end,
-- pokefirered/src/battle_message.c:2570
textX = (L + 1) * 8, textDy = 2,
}
end
-- pokeruby/src/script_menu.c:626
local function rsWidthTiles(labels)
local FrlgFont = require("src.ui.game3.frlg_font")
local w = 0
for _, lab in ipairs(labels) do
local px = FrlgFont.measure(tostring(lab or ""))
local tiles = math.floor((px + 7) / 8)
if tiles > w then w = tiles end
end
return w
end
-- pokeruby/src/menu.c:458
local function rsGridRows(n, cols)
if cols == 1 or cols == n or not (math.floor(n / 2) < cols or n % 2 ~= 0) then
return math.floor(n / cols)
end
return math.floor(n / cols) + 1
end
function Choice.rsMultiGeometry(labels, left, top, cols)
local n = #labels
local w = rsWidthTiles(labels)
local tx, ty = left, top
if cols <= 1 then
-- pokeruby/src/script_menu.c:647
if tx + w > 29 then tx = 29 - w end
local cells = {}
for i = 1, n do
cells[i] = { x = tx * 8, y = (ty + 2 * (i - 1)) * 8 }
end
return { frame = { tx, ty, w, 2 * n }, cells = cells, barWidth = w * 8 }
end
-- pokeruby/src/menu.c:454
local rows = rsGridRows(n, cols)
local total = cols * (w + 1) - 1
local cells = {}
for i = 1, n do
local c = (i - 1) % cols
local r = math.floor((i - 1) / cols)
cells[i] = { x = (tx + c * (w + 1)) * 8, y = (ty + 2 * r) * 8 }
end
return { frame = { tx, ty, total, 2 * rows }, cells = cells, barWidth = w * 8 }
end
-- pokeruby/src/script_menu.c:655, pokeruby/src/menu.c:524
function Choice.drawRsMulti()
local g = Choice.rsMultiGeometry(Choice.options, Choice.left or 20, Choice.top or 5, Choice.cols or 1)
Window.stdFrame(Window.template(g.frame[1], g.frame[2], g.frame[3], g.frame[4]))
for i, lab in ipairs(Choice.options) do
Window.printPx(lab, g.cells[i].x, g.cells[i].y)
end
local cur = g.cells[Choice.cursor] or g.cells[1]
if cur then
-- pokeruby/src/menu.c:748
require("src.ui.game3.rs.menu_cursor").draw(cur.x, cur.y, g.barWidth)
end
end
function Choice.draw()
if not Choice.active or not Choice.options then return end
if Choice.style == "battle" and Choice.kind == "yesno" then
-- pokefirered/src/battle_script_commands.c:9775
local L, Tp = Choice.left, Choice.top
local okB, BattleChrome = pcall(require, "src.ui.game3.battle_chrome")
local layout = okB and type(BattleChrome) == "table" and BattleChrome.layout and BattleChrome.layout() or "frlg"
local g = Choice.battleYesNoGeometry(layout, Choice.left, Choice.top)
local okR, Runtime = pcall(require, "src.core.game3.runtime")
local session = okR and type(Runtime) == "table" and Runtime.getSession and Runtime.getSession()
local opts = type(session) == "table" and session.options or nil
local frameType = tonumber(type(opts) == "table" and opts.frameType or nil) or 0
-- pokefirered/src/battle_bg.c:693
Window.userFrame(Window.template(L, Tp, 5, 4), frameType)
-- pokefirered/src/battle_bg.c:694
Window.userFrame(Window.template(g.frame.left, g.frame.top, g.frame.width, g.frame.height), frameType)
for i, lab in ipairs(Choice.options) do
local rowPx = (Tp + (i - 1) * 2) * 8
if i == Choice.cursor then Window.cursorPx(L * 8, rowPx) end
-- pokefirered/src/battle_message.c:2574
Window.printPx(lab, (L + 1) * 8, rowPx + 2)
local rowPx = g.rowY(i)
if i == Choice.cursor and g.cursor == "arrow" then Window.cursorPx(g.cursorX, rowPx) end
Window.printPx(lab, g.textX, rowPx + g.textDy)
end
if g.cursor == "rs" then
require("src.ui.game3.rs.menu_cursor").draw(g.cursorX, g.rowY(Choice.cursor), g.cursorWidth)
end
return
end
@@ -201,6 +336,11 @@ function Choice.draw()
return
end
if Choice.fieldLayout() == "rs" then
Choice.drawRsMulti()
return
end
local n = #Choice.options
local cols = Choice.cols or 1
if cols <= 1 then
+14 -1
View File
@@ -70,7 +70,20 @@ end
function CoinsBox.draw()
if not CoinsBox.visible then return end
if require("src.core.game3.profile").family() == "rse" then
local Profile = require("src.core.game3.profile")
local row = Profile.forSession()
local cw = type(row) == "table" and type(row.ui) == "table" and row.ui.coinsWindow or nil
if cw and cw.frameOrigin then
-- pokeruby/src/coins.c:16
local left, top = CoinsBox.x + 1, CoinsBox.y + 1
Window.stdFrame(Window.template(left, top, 8, 2))
-- pokeruby/src/coins.c:25
local digits = tostring(clamp(CoinsBox._amount))
local text = RomText.plain(cw.text or "gText_Coins", { stringVars = { digits } })
Window.printPx(text, left * 8 + 7 + math.max(0, 4 - #digits) * 6, top * 8)
return
end
if Profile.family() == "rse" then
-- pokeemerald/src/coins.c:14 PrintCoinsString, :24 ShowCoinsWindow
local tpl = Window.template(CoinsBox.x, CoinsBox.y, 8, 2)
Window.stdFrame(tpl)
+51 -3
View File
@@ -47,8 +47,9 @@ end
-- pokefirered/src/save_menu_util.c:25
function SaveMenu.countDex(session)
local dex = type(session) == "table" and session.dex or nil
if SaveMenu.layout(session) == "rse" then
if SaveMenu.isHoenn(session) then
-- pokeemerald/src/menu.c:2122
-- pokeruby/src/save_menu_util.c:118
local st = SaveMenu.flagStore(session)
return Dex.summaryCount({ version = session.version, dex = dex, flags = st.flags, vars = st.vars })
end
@@ -75,6 +76,10 @@ function SaveMenu.layout(session)
return ui and ui.saveMenu or "frlg"
end
function SaveMenu.isHoenn(session)
return SaveMenu.layout(session) ~= "frlg"
end
local function saveExists(session)
local ok, raw = pcall(require("src.core.SaveData").load, session and session.version)
return ok and type(raw) == "table"
@@ -85,7 +90,8 @@ function SaveMenu.show(opts)
SaveMenu.open = true
SaveMenu.cursor = 1
SaveMenu._phase = "confirm"
SaveMenu._rse = SaveMenu.layout(opts.session) == "rse"
SaveMenu._layout = SaveMenu.layout(opts.session)
SaveMenu._rse = SaveMenu._layout ~= "frlg"
SaveMenu._timer = nil
SaveMenu._error = nil
SaveMenu._session = opts.session
@@ -245,7 +251,7 @@ end
function SaveMenu.locationName(session)
session = session or {}
if SaveMenu.layout(session) == "rse" then return rseLocationName(session) end
if SaveMenu.isHoenn(session) then return rseLocationName(session) end
if type(session.mapName) == "string" and session.mapName ~= "" and not session.mapName:find("^FR_") and not session.mapName:find("^SEVII_") then
return session.mapName:upper()
end
@@ -310,7 +316,40 @@ function SaveMenu.drawRse()
local pt = session.playtime or session.playTime or {}
row(y, "gText_SavingTime", string.format("%d:%02d", tonumber(pt.hours or session.hours) or 0,
tonumber(pt.minutes or session.minutes) or 0))
SaveMenu.drawDialog(session)
end
-- pokeruby/src/save_menu_util.c:12
function SaveMenu.drawRs()
local session = SaveMenu._session or {}
local hasDex = SaveMenu.hasDex(session)
local win = Window.template(1, 1, 12, hasDex and 10 or 8)
Window.stdFrame(win)
local x0, y0 = win.left * 8, win.top * 8
local right = (win.left + 12) * 8
local NORMAL = FrlgFont.COLOR.NORMAL
local function row(y, labelKey, value)
FrlgFont.draw(RomText.plain(labelKey), x0, y0 + y, { colors = NORMAL })
-- pokeruby/src/save_menu_util.c:68
FrlgFont.draw(value, right - FrlgFont.measure(value), y0 + y, { colors = NORMAL })
end
-- pokeruby/src/save_menu_util.c:76
FrlgFont.draw(Strings(SaveMenu.locationName(session)), x0, y0, { colors = NORMAL })
row(16, "gText_SavingPlayer", tostring(session.name or session.playerName or ""))
row(32, "gText_SavingBadges", tostring(SaveMenu.countBadges(session)))
local y = 48
if hasDex then
row(y, "gText_SavingPokedex", tostring(SaveMenu.countDex(session)))
y = y + 16
end
local pt = session.playtime or session.playTime or {}
row(y, "gText_SavingTime", string.format("%d:%02d", tonumber(pt.hours or session.hours) or 0,
tonumber(pt.minutes or session.minutes) or 0))
SaveMenu.drawDialog(session)
end
function SaveMenu.drawDialog(session)
local NORMAL = FrlgFont.COLOR.NORMAL
Chrome.dialogueFrame()
local left, top, width = Chrome.dialogueWindow()
local key = ({ confirm = "gText_ConfirmSave", overwrite = "gText_AlreadySavedFile",
@@ -332,6 +371,14 @@ function SaveMenu.drawRse()
local yn = Window.template(21, 9, 5, 4)
Window.stdFrame(yn)
local yx, yy = yn.left * 8, yn.top * 8
if SaveMenu._layout == "rs" then
-- pokeruby/src/menu.c:602
FrlgFont.draw(RomText.plain("gText_Yes"), yx, yy, { colors = NORMAL })
FrlgFont.draw(RomText.plain("gText_No"), yx, yy + 16, { colors = NORMAL })
-- pokeruby/src/menu.c:721
require("src.ui.game3.rs.menu_cursor").draw(yx, yy + (SaveMenu.cursor - 1) * 16, 40)
return
end
FrlgFont.draw(RomText.plain("gText_Yes"), yx + 8, yy + 1, { colors = NORMAL })
FrlgFont.draw(RomText.plain("gText_No"), yx + 8, yy + 17, { colors = NORMAL })
Window.cursorPx(yx, yy + 1 + (SaveMenu.cursor == 2 and 16 or 0))
@@ -340,6 +387,7 @@ end
function SaveMenu.draw()
if not SaveMenu.open then return end
if SaveMenu._layout == "rs" then return SaveMenu.drawRs() end
if SaveMenu._rse then return SaveMenu.drawRse() end
local session = SaveMenu._session or {}
local name = tostring(session.name or session.playerName or "")
+11
View File
@@ -437,6 +437,17 @@ function StartMenu.draw()
local popY = 9
local popW = 6
local popH = 4
if d and d.drawCursor then
-- pokeruby/src/menu.c:608
Window.stdFrame(Window.template(popX, popY, 5, popH))
local cur = StartMenu._confirmCursor == 1 and 0 or 1
-- pokeruby/src/menu.c:602
FrlgFont.draw(RomText.plain("gText_Yes"), popX * 8, popY * 8, { colors = FrlgFont.COLOR.NORMAL })
FrlgFont.draw(RomText.plain("gText_No"), popX * 8, popY * 8 + 16, { colors = FrlgFont.COLOR.NORMAL })
-- pokeruby/src/menu.c:721, :750
require("src.ui.game3.rs.menu_cursor").draw(popX * 8, popY * 8 + cur * 16, 40)
return
end
Window.stdFrame(Window.template(popX, popY, popW, popH))
local rowY1 = popY * 8 + 2
local rowY2 = popY * 8 + 18
+151
View File
@@ -0,0 +1,151 @@
local Toast = {}
Toast.KINDS = {
ok = { color = "green", icon = "check", hold = 3.5 },
error = { color = "red", icon = "triangle-alert", hold = 7 },
warn = { color = "yellow", icon = "triangle-alert", hold = 6 },
info = { color = "blue", icon = "circle-help", hold = 5 },
}
Toast.SLIDE = .18
Toast.FADE = .25
local function now()
if love and love.timer and love.timer.getTime then return love.timer.getTime() end
return nil
end
local function holdOf(t)
if t.sticky then return math.huge end
return t.hold or Toast.KINDS[t.kind].hold
end
local function expired(t, at)
return t.at and at and (at - t.at) > holdOf(t) + Toast.FADE
end
function Toast.show(store, text, kind, opts)
if text == nil or text == "" then store.toast = nil; return end
text = tostring(text)
kind = Toast.KINDS[kind] and kind or "info"
opts = opts or {}
local t, at = store.toast, now()
if t and t.text == text and t.kind == kind and not expired(t, at) then
if t.at and at then t.at = at - math.min(Toast.SLIDE, at - t.at) end
t.sticky, t.hold = opts.sticky, opts.hold
return t
end
store.toast = { text = text, kind = kind, at = at, sticky = opts.sticky, hold = opts.hold }
return store.toast
end
function Toast.clear(store)
store.toast = nil
end
function Toast.current(store)
local t = store and store.toast
if not t then return nil end
if expired(t, now()) then store.toast = nil; return nil end
return t
end
function Toast.hit(store, x, y)
local t = Toast.current(store)
local r = t and t.rect
if r and t.at then
local at = now()
local elapsed = at and (at - t.at)
if elapsed and (elapsed < Toast.SLIDE or elapsed > holdOf(t)) then return false end
end
return r ~= nil and x ~= nil and y ~= nil
and x >= r[1] and x <= r[1] + r[3] and y >= r[2] and y <= r[2] + r[4]
end
local function wrapLines(font, text, w)
if not font then return { text } end
local ok, _, lines = pcall(font.getWrap, font, text, w)
return (ok and lines and #lines > 0) and lines or { text }
end
local function lineHeight(font)
return font and font:getHeight() or 12
end
local function cachedLines(t, font, w)
local c = t._wrap
if c and c.font == font and c.w == w then return c.lines end
local lines = wrapLines(font, t.text, w)
t._wrap = { font = font, w = w, lines = lines }
return lines
end
local function defaultText(font, lines, x, y, w, color, maxLines)
local G = love and love.graphics
if not G or not font then return end
local Theme = require("src.ui.kit.Theme")
local lh = lineHeight(font)
G.setFont(font)
Theme.col(color, 1)
for i = 1, math.min(#lines, maxLines) do
local line = lines[i]
if i == maxLines and #lines > maxLines then line = Theme.ellipsize(font, line .. "...", w) end
pcall(G.print, line, Theme.snap(x), Theme.snap(y + (i - 1) * lh))
end
end
function Toast.draw(store, area)
local t = store and store.toast
if not t then return nil end
local at = now()
if not t.at then t.at = at end
local elapsed = (at and t.at) and (at - t.at) or 0
local hold = holdOf(t)
if elapsed > hold + Toast.FADE then store.toast = nil; return nil end
local Theme = require("src.ui.kit.Theme")
local Icons = require("src.ui.kit.Icons")
local PAL = Theme.PAL
local style = Toast.KINDS[t.kind]
local slide = math.min(1, elapsed / Toast.SLIDE, (hold + Toast.FADE - elapsed) / Toast.FADE)
slide = 1 - (1 - math.max(0, slide)) ^ 3
local s, color = area.s or 1, PAL[style.color]
local pad, gap, icon = 12 * s, 10 * s, 20 * s
local maxLines = area.maxLines or 4
local w = area.width or math.min(420 * s, area.w - 32 * s)
local textW = w - 2 * pad - icon - gap
local lines = (not area.wrapHeight or not area.drawText) and cachedLines(t, area.font, textW)
local textH = area.wrapHeight and area.wrapHeight(t.text, textW, maxLines)
or math.min(#lines, maxLines) * lineHeight(area.font)
local h = 2 * pad + math.max(icon, textH)
local x = (area.x or 0) + (area.w - w) / 2
local y, fromAbove
if area.place then y, fromAbove = area.place(x, w, h) end
if y == nil then
if area.centerY then
y, fromAbove = area.centerY - h / 2, true
elseif area.top and not (area.bottom and area.maxY and area.top + 12 * s + h > area.maxY) then
y, fromAbove = area.top + 12 * s, true
else
y, fromAbove = area.bottom - h - 12 * s, false
end
end
if fromAbove then
y = y - (1 - slide) * (h + 24 * s)
else
y = y + (1 - slide) * (h + 24 * s)
end
t.rect = { x, y, w, h }
local r = area.radius or Theme.cardRadius()
Theme.fillRounded(x + 2 * s, y + 4 * s, w, h, PAL.inverse, .35, r)
Theme.fillRounded(x, y, w, h, PAL.surface, .98, r)
Theme.fillRounded(x, y, w, h, color, .16, r)
Theme.strokeRounded(x, y, w, h, color, .75, 1.5, r)
Icons.draw(style.icon, x + pad, y + (h - icon) / 2, icon, color, 1)
if area.drawText then
area.drawText(t.text, x + pad + icon + gap, y + pad, textW, PAL.heading, maxLines)
else
defaultText(area.font, lines, x + pad + icon + gap, y + pad, textW, PAL.heading, maxLines)
end
return t.rect
end
return Toast
+108
View File
@@ -0,0 +1,108 @@
function love.run()
_G.POKEPORT_LOOP_RESTART = true
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
_G.POKEPORT_LOOP_PANEL_SYNC = true
FrameCap.bootPanelSync()
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local function sleepUntilFrame(deadline)
while true do
local remaining = deadline - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
if remaining > 0.004 then
love.timer.sleep(remaining - 0.002)
else
love.timer.sleep(remaining)
end
end
end
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if a ~= "restart" and love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
if name == "focus" and a then
PresentSync.onDisplayChange()
elseif name == "resize" then
PresentSync.onDisplayChange()
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
RefreshRate.sample(dt)
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
else
local idleCap = idlePresentationCap(idleFor)
if idleCap then cap = idleCap end
end
if cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
elseif cap == FrameCap.DISPLAY and PresentSync.needsSoftwareCap() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
PresentSync.waitBeforePresent()
love.graphics.present()
PresentSync.notePresent()
end
PresentSync.applyFixedStepPeriod()
if love.timer then
if paced and cap ~= FrameCap.DISPLAY and not PresentSync.hardwarePacesCap(cap) then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
sleepUntilFrame(nextFrame)
else
love.timer.sleep(0.001)
end
end
end
end
+46
View File
@@ -0,0 +1,46 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a or 0
end
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
if love.update then love.update(dt) end
if love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then
if paced then
local budget = 1 / FrameCap.current
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= 0 then break end
love.timer.sleep(remaining < 0.001 and remaining or 0.001)
end
else
love.timer.sleep(0.001)
end
end
end
end
+49
View File
@@ -0,0 +1,49 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
if love.update then love.update(dt) end
if love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then
if paced then
local budget = 1 / FrameCap.current
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= 0 then break end
love.timer.sleep(remaining < 0.001 and remaining or 0.001)
end
else
love.timer.sleep(0.001)
end
end
end
end
+76
View File
@@ -0,0 +1,76 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then
if paced then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
love.timer.sleep(0.001)
end
else
love.timer.sleep(0.001)
end
end
end
end
+77
View File
@@ -0,0 +1,77 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then
if paced then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
love.timer.sleep(0.001)
end
else
love.timer.sleep(0.001)
end
end
end
end
+85
View File
@@ -0,0 +1,85 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
if RefreshRate.sample(dt) then
FixedStep.refreshPeriod = RefreshRate.period()
end
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
elseif cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then
if paced and cap ~= FrameCap.DISPLAY then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
love.timer.sleep(0.001)
end
else
love.timer.sleep(0.001)
end
end
end
end
+94
View File
@@ -0,0 +1,94 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
if name == "focus" and a then
PresentSync.onDisplayChange()
elseif name == "resize" then
PresentSync.onDisplayChange()
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
RefreshRate.sample(dt)
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
elseif cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
elseif cap == FrameCap.DISPLAY and PresentSync.needsSoftwareCap() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
PresentSync.waitBeforePresent()
love.graphics.present()
PresentSync.notePresent()
end
PresentSync.applyFixedStepPeriod()
if love.timer then
if paced and cap ~= FrameCap.DISPLAY then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
love.timer.sleep(0.001)
end
else
love.timer.sleep(0.001)
end
end
end
end
+106
View File
@@ -0,0 +1,106 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
FrameCap.bootHandheld()
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local function sleepUntilFrame(deadline)
while true do
local remaining = deadline - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
if remaining > 0.004 then
love.timer.sleep(remaining - 0.002)
else
love.timer.sleep(remaining)
end
end
end
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
if name == "focus" and a then
PresentSync.onDisplayChange()
elseif name == "resize" then
PresentSync.onDisplayChange()
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
RefreshRate.sample(dt)
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
else
local idleCap = idlePresentationCap(idleFor)
if idleCap then cap = idleCap end
end
if cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
elseif cap == FrameCap.DISPLAY and PresentSync.needsSoftwareCap() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
PresentSync.waitBeforePresent()
love.graphics.present()
PresentSync.notePresent()
end
PresentSync.applyFixedStepPeriod()
if love.timer then
if paced and cap ~= FrameCap.DISPLAY and not PresentSync.hardwarePacesCap(cap) then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
sleepUntilFrame(nextFrame)
else
love.timer.sleep(0.001)
end
end
end
end
+106
View File
@@ -0,0 +1,106 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
FrameCap.bootPanelSync()
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local function sleepUntilFrame(deadline)
while true do
local remaining = deadline - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
if remaining > 0.004 then
love.timer.sleep(remaining - 0.002)
else
love.timer.sleep(remaining)
end
end
end
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
if name == "focus" and a then
PresentSync.onDisplayChange()
elseif name == "resize" then
PresentSync.onDisplayChange()
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
RefreshRate.sample(dt)
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
else
local idleCap = idlePresentationCap(idleFor)
if idleCap then cap = idleCap end
end
if cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
elseif cap == FrameCap.DISPLAY and PresentSync.needsSoftwareCap() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
PresentSync.waitBeforePresent()
love.graphics.present()
PresentSync.notePresent()
end
PresentSync.applyFixedStepPeriod()
if love.timer then
if paced and cap ~= FrameCap.DISPLAY and not PresentSync.hardwarePacesCap(cap) then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
sleepUntilFrame(nextFrame)
else
love.timer.sleep(0.001)
end
end
end
end
+107
View File
@@ -0,0 +1,107 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
_G.POKEPORT_LOOP_PANEL_SYNC = true
FrameCap.bootPanelSync()
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local function sleepUntilFrame(deadline)
while true do
local remaining = deadline - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
if remaining > 0.004 then
love.timer.sleep(remaining - 0.002)
else
love.timer.sleep(remaining)
end
end
end
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
if name == "focus" and a then
PresentSync.onDisplayChange()
elseif name == "resize" then
PresentSync.onDisplayChange()
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
RefreshRate.sample(dt)
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
else
local idleCap = idlePresentationCap(idleFor)
if idleCap then cap = idleCap end
end
if cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
elseif cap == FrameCap.DISPLAY and PresentSync.needsSoftwareCap() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
PresentSync.waitBeforePresent()
love.graphics.present()
PresentSync.notePresent()
end
PresentSync.applyFixedStepPeriod()
if love.timer then
if paced and cap ~= FrameCap.DISPLAY and not PresentSync.hardwarePacesCap(cap) then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
sleepUntilFrame(nextFrame)
else
love.timer.sleep(0.001)
end
end
end
end
+107
View File
@@ -0,0 +1,107 @@
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local FrameCap = require("src.core.FrameCap")
_G.POKEPORT_LOOP_PANEL_SYNC = true
FrameCap.bootPanelSync()
local RefreshRate = require("src.core.RefreshRate")
local FixedStep = require("src.core.FixedStep")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local paced = pacingEnabled()
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local function sleepUntilFrame(deadline)
while true do
local remaining = deadline - love.timer.getTime()
if remaining <= SLEEP_FLOOR then break end
if remaining > 0.004 then
love.timer.sleep(remaining - 0.002)
else
love.timer.sleep(remaining)
end
end
end
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
if love.event then
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
if a ~= "restart" and love.system and love.system.getOS() == "Android" then
os.exit(a or 0)
end
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
if name == "focus" and a then
PresentSync.onDisplayChange()
elseif name == "resize" then
PresentSync.onDisplayChange()
end
love.handlers[name](a, b, c, d, e, f)
end
end
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
RefreshRate.sample(dt)
checkEmergencyQuit(dt)
if love.update then love.update(dt) end
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
else
local idleCap = idlePresentationCap(idleFor)
if idleCap then cap = idleCap end
end
if cap == FrameCap.DISPLAY and not VSync.isOn() then
cap = FrameCap.DEFAULT
elseif cap == FrameCap.DISPLAY and PresentSync.needsSoftwareCap() then
cap = FrameCap.DEFAULT
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
PresentSync.waitBeforePresent()
love.graphics.present()
PresentSync.notePresent()
end
PresentSync.applyFixedStepPeriod()
if love.timer then
if paced and cap ~= FrameCap.DISPLAY and not PresentSync.hardwarePacesCap(cap) then
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
if now - nextFrame > budget then
nextFrame = now
end
sleepUntilFrame(nextFrame)
else
love.timer.sleep(0.001)
end
end
end
end
+1 -1
View File
@@ -117,7 +117,7 @@ return function(game)
U.wait(60)
end
result(BattleChrome.isRse(), "Emerald chrome manifest is the rse layout")
result(BattleChrome.layout() == "emerald", "Emerald chrome manifest is the emerald layout")
load_map("EM_ROUTE117", 32, 15)
set_party()
@@ -0,0 +1,133 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_secret_base_poison_gate"
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
print((failures == 0 and "PASS" or "FAIL") .. " em_secret_base_poison_gate failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
U.wait(10)
end
return function(game)
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
if not check(game.boot ~= nil, "boot reached") then return finish() end
game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 })
U.wait(60)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local MapIds = require("src.core.game3.map_ids")
local Player = require("src.core.game3.player")
local Message = require("src.ui.game3.message")
local Party = require("src.core.game3.party")
local Field = require("src.core.game3.field")
local Space = require("src.core.game3.scripting.space")
local Warp = require("src.core.game3.warp")
local StepEvents = require("src.core.game3.step_events")
local Sem = require("src.core.game3.field_semantics")
local session = Runtime.getSession()
if not check(session ~= nil, "field session exists") then return finish() end
local C = require("src.core.game3.constants").active(session)
local id = require("src.core.game3.profile").forSession(session).id
local tag = id .. ": "
for _ = 1, 600 do
if not (Space.vm and Space.vm:isRunning()) and not Message.isOpen() and not Warp.isBusy() then break end
if Message.isOpen() then U.tap(game, "a") end
U.wait(2)
end
local function settle()
for _ = 1, 300 do
if not (Space.vm and Space.vm:isRunning()) and not Message.isOpen() and not Warp.isBusy()
and not StepEvents.busy() then return end
if Message.isOpen() then U.tap(game, "a") end
U.wait(2)
end
end
local function poisonParty()
session.party = {}
Party.giveMon(session, C:require("species", "SPECIES_TORCHIC"), 30)
local mon = session.party[1]
mon.status = "PSN"
mon.hp = mon.maxHp or mon.hp
return mon
end
local function setSteps(n)
session.vars[Sem.var(session, "poisonSteps")] = n
session.poisonSteps = n
end
local function stepDir(dir)
local sx, sy = Player.cellX, Player.cellY
U.hold(game, dir, 20)
U.wait(10)
settle()
return Player.cellX ~= sx or Player.cellY ~= sy
end
local okRun, err = pcall(function()
local base = MapIds.forConst("MAP_SECRET_BASE_BLUE_CAVE1", id)
local def = game.data.maps[base]
local w = def and def.warps and def.warps[1]
check(w ~= nil, tag .. "secret base map " .. tostring(base) .. " has an exit warp")
Map.load(nil, game, base, { x = w.x, y = w.y, facing = "up" })
session.x, session.y, session.facing = w.x, w.y, "up"
Field.unlock()
U.wait(30)
settle()
local cur = Map.currentDef()
check(Map.current == base, tag .. "inside " .. tostring(base))
check(cur and tonumber(cur.mapType) == 9, tag .. "map header mapType is MAP_TYPE_SECRET_BASE ("
.. tostring(cur and cur.mapType) .. ")")
local mon = poisonParty()
local hp0 = mon.hp
setSteps(3)
local moved = 0
if stepDir("up") then moved = moved + 1 end
for i = 1, 7 do
if stepDir(i % 2 == 1 and "up" or "down") then moved = moved + 1 end
end
print("INFO inside steps=" .. moved .. " hp=" .. tostring(mon.hp) .. "/" .. tostring(hp0))
check(moved >= 6, tag .. "walked " .. moved .. " steps inside the base")
check(Map.current == base, tag .. "still inside the base")
check(mon.hp == hp0, tag .. "HP unchanged inside the secret base (" .. tostring(mon.hp) .. ")")
check(tonumber(session.vars[Sem.var(session, "poisonSteps")]) == 3, tag .. "poison counter held at 3")
U.still(game, DIR .. "/s1_01_" .. id .. "_secret_base_poisoned_full_hp.png")
local route = MapIds.forConst("MAP_ROUTE103", id)
Map.load(nil, game, route, { x = 10, y = 10, facing = "down" })
session.x, session.y, session.facing = 10, 10, "down"
Field.unlock()
U.wait(30)
settle()
mon.hp = hp0
mon.status = "PSN"
setSteps(0)
local out = 0
local dirs = { "down", "up", "left", "right" }
for i = 1, 16 do
if out >= 8 then break end
if stepDir(dirs[(i - 1) % 4 + 1]) then out = out + 1 end
end
print("INFO outside steps=" .. out .. " hp=" .. tostring(mon.hp) .. "/" .. tostring(hp0))
check(out >= 8, tag .. "walked " .. out .. " steps on route 103")
check(mon.hp == hp0 - 2, tag .. "HP drops 2 after 8 steps outside (" .. tostring(mon.hp) .. ")")
U.still(game, DIR .. "/s1_02_" .. id .. "_route103_poison_hp_drop.png")
end)
check(okRun, tag .. "no crash " .. tostring(okRun and "" or err))
return finish()
end
@@ -48,7 +48,7 @@ return function(game)
session.money = 3000
Field.setRespawn(2)
local loss = math.min(BattleBridge.calcMoneyLossFrlg(session, game.save), 3000)
session.vars[0x4040] = 3
session.vars[0x4040] = 4 -- pokefirered/src/field_control_avatar.c:718
local startX, startY = Player.cellX, Player.cellY
for _, dir in ipairs({ "left", "right", "down", "up" }) do
@@ -75,9 +75,10 @@ return function(game)
local F = require(P.font.module)
local drawn
local orig = BattleChrome.drawMenuFrames
BattleChrome.drawMenuFrames = function(mode, layout)
drawn = BattleChrome.menuFrameRects(mode, layout)
return orig(mode, layout)
result(BattleChrome.layout() == "rs", "battle chrome layout is rs")
BattleChrome.drawMenuFrames = function(mode)
drawn = BattleChrome.menuFrameRects(mode, BattleChrome.layout())
return orig(mode)
end
for _ = 1, 2000 do
if drawn then break end
@@ -0,0 +1,106 @@
local U = require("tests.drivers.util")
local X = require("tests.drivers.em_xa_util")
local d = X.new("refix2_rs_prize_multichoice", "/tmp/refix2_rs_prize_multichoice")
return function(game)
local version = os.getenv("POKEPORT_VERSION") or "sapphire"
local rs = version == "ruby" or version == "sapphire"
local sess = X.newGame(d, game, 0)
if not sess then return d.finish() end
X.settle(game)
U.wait(30)
local C = require("src.core.game3.constants").of(version)
local Bag = require("src.core.game3.bag")
local MapIds = require("src.core.game3.map_ids")
local Window = require("src.ui.game3.window")
local MenuCursor = require("src.ui.game3.rs.menu_cursor")
local Chrome = require("src.ui.game3.chrome")
local Choice = require("src.ui.game3.choice")
local FrlgFont = require("src.ui.game3.frlg_font")
Bag.add(sess.bag, C:require("items", "ITEM_COIN_CASE"), 1)
Bag.Coins.set(sess, 120)
local arrows, bars, frames = {}, {}, {}
local origArrow, origBar, origStd = Window.cursorPx, MenuCursor.draw, Chrome.stdFrame
Window.cursorPx = function(x, y, ...)
arrows[#arrows + 1] = { x, y }
return origArrow(x, y, ...)
end
MenuCursor.draw = function(x, y, w, ...)
bars[#bars + 1] = { x, y, w }
return origBar(x, y, w, ...)
end
Chrome.stdFrame = function(l, t, w, h, ...)
frames[#frames + 1] = { l, t, w, h }
return origStd(l, t, w, h, ...)
end
-- pokeruby/data/maps/MauvilleCity_GameCorner/scripts.inc:121 multichoice 12, 0, 48, 0
local corner = MapIds.forConst("MAP_MAUVILLE_CITY_GAME_CORNER")
if not d.check(X.goTo(d, game, corner, 14, 3, "up"), "Mauville Game Corner loads") then return d.finish() end
U.wait(20)
U.tap(game, "a")
local opened = X.waitFor(function()
if Choice.isOpen() and Choice.kind == "multi" then return true end
local Message = require("src.ui.game3.message")
if Message.isOpen and Message.isOpen() and U.frame() % 20 == 0 then U.tap(game, "a") end
return false
end, 3000)
if not d.check(opened, "prize clerk script opens multichoice 48") then return d.finish() end
d.check(Choice.left == 13 and Choice.top == 1, string.format("multichoice 12,0 interior at %s,%s", tostring(Choice.left), tostring(Choice.top)))
local L, Tp = Choice.left, Choice.top
arrows, bars, frames = {}, {}, {}
local n = #(Choice.options or {})
X.waitFor(function()
for _, f in ipairs(frames) do if f[4] == 2 * n then return true end end
return false
end, 600)
U.wait(10)
local w = 0
for _, lab in ipairs(Choice.options or {}) do
local tiles = math.floor((FrlgFont.measure(tostring(lab)) + 7) / 8)
if tiles > w then w = tiles end
end
local tx = L + w > 29 and 29 - w or L
d.note(string.format("%d options, widest %d tiles, frame left %d", n, w, tx))
local fr
for _, f in ipairs(frames) do
d.note(string.format("frame %d,%d %dx%d", f[1], f[2], f[3], f[4]))
if f[4] == 2 * n then fr = f end
end
d.check(fr ~= nil, "multichoice drew a std frame 2*count tall")
if fr then
d.check(fr[1] == tx and fr[2] == Tp and fr[3] == w,
string.format("multichoice frame interior %d,%d %dx%d (pret left+1, top+1, widest label, 2*count)", fr[1], fr[2], fr[3], fr[4]))
end
if rs then
local bar = bars[#bars]
d.check(#arrows == 0, "RS multichoice draws no FRLG arrow glyph")
d.check(bar ~= nil and bar[1] == tx * 8 and bar[2] == Tp * 8 and bar[3] == w * 8,
string.format("RS multichoice highlight bar at (%d, %d) width %d on item 0", tx * 8, Tp * 8, w * 8))
else
d.check(#bars == 0 and #arrows > 0, "FRLG/Emerald multichoice keeps the arrow glyph")
end
d.still(game, rs and "refix2_01_rs_prize_multichoice_bar_on_item0.png" or "refix2_03_em_prize_multichoice.png")
U.tap(game, "down")
U.wait(10)
d.check(Choice.cursor == 2, "cursor moved to item 1")
if rs then
X.waitFor(function() return bars[#bars] and bars[#bars][2] == (Tp + 2) * 8 end, 600)
local bar = bars[#bars]
d.check(bar ~= nil and bar[2] == (Tp + 2) * 8, string.format("RS multichoice highlight bar at y %d on item 1", (Tp + 2) * 8))
end
d.still(game, rs and "refix2_02_rs_prize_multichoice_bar_on_item1.png" or "refix2_04_em_prize_multichoice_item1.png")
Window.cursorPx, MenuCursor.draw, Chrome.stdFrame = origArrow, origBar, origStd
for _ = 1, 60 do
if not (X.scriptRunning and X.scriptRunning()) then break end
U.tap(game, "b")
U.wait(15)
end
d.finish()
end
+63
View File
@@ -0,0 +1,63 @@
local U = require("tests.drivers.util")
local X = require("tests.drivers.em_xa_util")
local d = X.new("s2_coins_box", "/tmp/s2_coins_box")
return function(game)
local version = os.getenv("POKEPORT_VERSION") or "sapphire"
local rs = version == "ruby" or version == "sapphire"
local sess = X.newGame(d, game, 0)
if not sess then return d.finish() end
X.settle(game)
U.wait(30)
local C = require("src.core.game3.constants").of(version)
local Bag = require("src.core.game3.bag")
local MapIds = require("src.core.game3.map_ids")
local CoinsBox = require("src.ui.game3.coins_box")
local Chrome = require("src.ui.game3.chrome")
Bag.add(sess.bag, C:require("items", "ITEM_COIN_CASE"), 1)
Bag.Coins.set(sess, 120)
local corner = MapIds.forConst("MAP_MAUVILLE_CITY_GAME_CORNER")
if not d.check(X.goTo(d, game, corner, 14, 3, "up"), "Mauville Game Corner loads") then return d.finish() end
U.wait(20)
U.tap(game, "a")
local shown = X.waitFor(function()
if CoinsBox.isVisible() then return true end
local Message = require("src.ui.game3.message")
if Message.isOpen and Message.isOpen() and U.frame() % 20 == 0 then U.tap(game, "a") end
return false
end, 3000)
if not d.check(shown, "doll prize clerk script runs showcoinsbox") then return d.finish() end
d.check(CoinsBox.x == (rs and 0 or 1) and CoinsBox.y == (rs and 0 or 1),
string.format("script origin is %d,%d", CoinsBox.x, CoinsBox.y))
local frames = {}
local origStd = Chrome.stdFrame
Chrome.stdFrame = function(l, t, w, h, ...)
frames[#frames + 1] = { l, t, w, h }
return origStd(l, t, w, h, ...)
end
local box
X.waitFor(function()
for _, f in ipairs(frames) do
if f[3] == 8 and f[4] == 2 then box = f return true end
end
return false
end, 2000)
Chrome.stdFrame = origStd
d.check(box ~= nil, "coins window drew an 8x2 std frame")
if box then
d.note(string.format("coins interior %d,%d", box[1], box[2]))
d.check(box[1] == 1 and box[2] == 1, "coins frame top-left is tile 0,0 (interior at 1,1)")
end
U.wait(30)
d.still(game, rs and "S2_02_rs_coins_box.png" or "S2_04_em_coins_box.png")
for _ = 1, 60 do
if not (X.scriptRunning and X.scriptRunning()) then break end
U.tap(game, "b")
U.wait(15)
end
d.finish()
end
+110
View File
@@ -0,0 +1,110 @@
local U = require("tests.drivers.util")
local X = require("tests.drivers.em_xa_util")
local d = X.new("s2_rs_field_yesno", "/tmp/s2_rs_field_yesno")
return function(game)
local version = os.getenv("POKEPORT_VERSION") or "sapphire"
local rs = version == "ruby" or version == "sapphire"
local sess = X.newGame(d, game, 0)
if not sess then return d.finish() end
X.settle(game)
U.wait(30)
local Window = require("src.ui.game3.window")
local MenuCursor = require("src.ui.game3.rs.menu_cursor")
local Chrome = require("src.ui.game3.chrome")
local Choice = require("src.ui.game3.choice")
local Message = require("src.ui.game3.message")
local arrows, bars, frames = {}, {}, {}
local origArrow, origBar, origStd = Window.cursorPx, MenuCursor.draw, Chrome.stdFrame
Window.cursorPx = function(x, y, ...)
arrows[#arrows + 1] = { x, y }
return origArrow(x, y, ...)
end
MenuCursor.draw = function(x, y, w, ...)
bars[#bars + 1] = { x, y, w }
return origBar(x, y, w, ...)
end
Chrome.stdFrame = function(l, t, w, h, ...)
frames[#frames + 1] = { l, t, w, h }
return origStd(l, t, w, h, ...)
end
local emerald = version == "emerald"
-- pokeruby/src/scrcmd.c:1277 yesnobox 19, 7
Message.showStay("Would you like to save\nthe game?")
U.wait(10)
local answered
Choice.yesNo(function(yes) answered = yes end, { left = 19, top = 7 })
d.check(Choice.fieldLayout() == (rs and "rs" or "frlg"), "field yes/no layout is " .. tostring(Choice.fieldLayout()))
arrows, bars, frames = {}, {}, {}
X.waitFor(function() return #frames > 0 end, 600)
U.wait(10)
local yn
for _, f in ipairs(frames) do if f[4] == 4 then yn = f end end
d.check(yn ~= nil, "script yes/no drew a std frame")
if yn then
d.note(string.format("yes/no interior %d,%d %dx%d", yn[1], yn[2], yn[3], yn[4]))
if rs then
-- pokeruby/src/script_menu.c:765
d.check(yn[1] == 20 and yn[2] == 8, string.format("RS yesnobox 19,7 interior at tile %d,%d", yn[1], yn[2]))
d.check(yn[3] == 5, "RS yesnobox interior width " .. yn[3])
else
-- pokeemerald/src/script_menu.c:198, pokefirered/src/script_menu.c:864
d.check(yn[1] == 21 and yn[2] == 9, string.format("yesnobox 19,7 ignored: interior at tile %d,%d", yn[1], yn[2]))
d.check(yn[3] == (emerald and 5 or 6), "yesnobox interior width " .. yn[3])
end
end
if rs then
local bar = bars[#bars]
d.check(#arrows == 0, "RS script yes/no draws no FRLG arrow glyph")
d.check(bar ~= nil and bar[1] == 160 and bar[2] == 64 and bar[3] == 40,
"RS script yes/no highlight bar at (160, 64) width 40 on YES")
else
d.check(#bars == 0 and #arrows > 0, "FRLG/Emerald script yes/no keeps the arrow glyph")
end
d.still(game, rs and "S2_05_rs_script_yesno_bar_on_yes.png" or "S2_07_em_script_yesno.png")
U.tap(game, "down")
U.wait(10)
d.check(Choice.cursor == 2, "cursor moved to NO")
if rs then
X.waitFor(function() return bars[#bars] and bars[#bars][2] == 80 end, 600)
local bar = bars[#bars]
d.check(bar ~= nil and bar[2] == 80, "RS script yes/no highlight bar at y 80 on NO")
end
d.still(game, rs and "S2_06_rs_script_yesno_bar_on_no.png" or "S2_08_em_script_yesno_no.png")
U.tap(game, "a")
U.wait(10)
d.check(answered == false, "NO answered false")
if Message.isOpen() then Message.close() end
U.wait(10)
local StartMenu = require("src.ui.game3.screens").get("start_menu", sess)
StartMenu.show({ session = sess, game = game })
U.wait(10)
StartMenu._confirmExit = true
StartMenu._confirmCursor = 2
arrows, bars, frames = {}, {}, {}
X.waitFor(function() return #frames > 1 end, 600)
U.wait(10)
local ex
for _, f in ipairs(frames) do if f[2] == 9 and f[4] == 4 then ex = f end end
d.check(ex ~= nil, "exit confirm drew a yes/no frame")
if ex then d.check(ex[1] == 21 and ex[3] == (rs and 5 or 6), string.format("exit confirm interior %d,%d %dx%d", ex[1], ex[2], ex[3], ex[4])) end
if rs then
local bar = bars[#bars]
d.check(bar ~= nil and bar[1] == 168 and bar[2] == 88 and bar[3] == 40,
"RS exit confirm highlight bar at (168, 88) width 40 on NO")
local arrowInBox = false
for _, a in ipairs(arrows) do if a[2] >= 72 and a[2] <= 104 then arrowInBox = true end end
d.check(not arrowInBox, "RS exit confirm draws no FRLG arrow glyph")
else
d.check(#bars == 0, "Emerald exit confirm draws no RS bar")
end
d.still(game, rs and "S2_09_rs_exit_confirm_bar_on_no.png" or "S2_10_em_exit_confirm.png")
Window.cursorPx, MenuCursor.draw, Chrome.stdFrame = origArrow, origBar, origStd
StartMenu.close(true)
d.finish()
end
+69
View File
@@ -0,0 +1,69 @@
local U = require("tests.drivers.util")
local X = require("tests.drivers.em_xa_util")
local d = X.new("s2_save_window", "/tmp/s2_save_window")
return function(game)
local version = os.getenv("POKEPORT_VERSION") or "sapphire"
local rs = version == "ruby" or version == "sapphire"
local sess = X.newGame(d, game, 0)
if not sess then return d.finish() end
X.settle(game)
U.wait(30)
local Space = require("src.core.game3.scripting.space")
local Flags = require("src.core.game3.scripting.flags")
local dexFlag = require("src.ui.game3.screens").flags(sess).IDS.SYS_POKEDEX_GET
Flags.setFlag(Space.store, nil, dexFlag, true)
local Chrome = require("src.ui.game3.chrome")
local Window = require("src.ui.game3.window")
local MenuCursor = require("src.ui.game3.rs.menu_cursor")
local SaveMenu = require("src.ui.game3.save_menu")
local frames, arrows, bars = {}, {}, {}
local origStd = Chrome.stdFrame
Chrome.stdFrame = function(l, t, w, h, ...)
frames[#frames + 1] = { l, t, w, h }
return origStd(l, t, w, h, ...)
end
local origArrow, origBar = Window.cursorPx, MenuCursor.draw
Window.cursorPx = function(x, y, ...)
arrows[#arrows + 1] = { x, y }
return origArrow(x, y, ...)
end
MenuCursor.draw = function(x, y, w, ...)
bars[#bars + 1] = { x, y, w }
return origBar(x, y, w, ...)
end
SaveMenu.show({ session = sess, game = game })
U.wait(20)
frames, arrows, bars = {}, {}, {}
X.waitFor(function() return #frames > 0 end, 2000)
if rs then
-- pokeruby/src/start_menu.c:699, pokeruby/src/menu.c:721, :750
local bar = bars[#bars]
d.check(#arrows == 0, "RS save Yes/No draws no FRLG arrow glyph")
d.check(bar ~= nil and bar[1] == 168 and bar[2] == 72 and bar[3] == 40,
"RS save Yes/No highlight bar at (168, 72) width 40 on YES")
else
d.check(#bars == 0 and #arrows > 0, "Emerald save Yes/No keeps the arrow glyph")
end
d.check(SaveMenu._layout == (rs and "rs" or "rse"), "save menu layout is " .. tostring(SaveMenu._layout))
local stats = frames[1]
d.check(stats ~= nil, "save stats window drew a std frame")
if stats then
d.note(string.format("stats frame interior %d,%d %dx%d", stats[1], stats[2], stats[3], stats[4]))
d.check(stats[1] == 1 and stats[2] == 1, "stats window interior starts at tile 1,1")
local want = rs and 12 or 14
d.check(stats[3] == want, string.format("stats window interior is %d tiles wide (frame right tile %d)", want, want + 1))
d.check(stats[4] == 10, "stats window with the dex is 10 tiles tall")
end
d.still(game, (rs and "S2_01_rs_save_stats_window.png" or "S2_03_em_save_stats_window.png"))
Chrome.stdFrame = origStd
Window.cursorPx, MenuCursor.draw = origArrow, origBar
SaveMenu.cancel()
U.wait(20)
d.check(not SaveMenu.isOpen(), "save dialog closes on B")
d.finish()
end
+115
View File
@@ -0,0 +1,115 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/BSA/10-07-26-01-sidebugs/shots/s7_battle_yesno_frame"
return function(game)
local fails = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then fails = fails + 1 end
end
local function finish() love.event.quit(fails == 0 and 0 or 1) end
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "TAI", gender = 0 })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local Party = require("src.core.game3.party")
local BattleBridge = require("src.core.game3.battle_bridge")
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local BattleText = require("src.core.game3.battle.battle_text")
local Choice = require("src.ui.game3.choice")
local BattleChrome = require("src.ui.game3.battle_chrome")
local version = os.getenv("POKEPORT_VERSION") or "emerald"
local C = require("src.core.game3.constants").of(version)
local session = Runtime.getSession()
result(session ~= nil, "field session exists")
if not session then finish() return end
local function sp(name) return C.species.byName["SPECIES_" .. name] end
session.party = {}
Party.giveMon(session, sp("TREECKO"), 11, "TREECKO")
local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = sp("WURMPLE"), level = 3 }, {})
result(ok == true, "battle started " .. tostring(err or ""))
if not ok then finish() return end
local lastTap, f = 0, 0
local function at_command()
return Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu"
end
for _ = 1, 6000 do
if at_command() then break end
f = f + 1
if Ui.dialogPending and Ui.dialogPending() and f - lastTap >= 14 then
lastTap = f
U.tap(game, "a")
else
U.wait(1)
end
end
result(at_command(), "reached command menu")
if not at_command() then finish() return end
U.wait(30)
local layout = BattleChrome.layout()
local expect = ({ frlg = "frlg", emerald = "emerald", rs = "rs" })[layout]
print("layout " .. tostring(layout))
result((version == "firered" or version == "leafgreen") == (layout == "frlg"), "battle chrome layout matches version (" .. tostring(layout) .. ")")
Ui._mode = "none"
Ui.askYesNo(BattleText.get("STRINGID_GIVENICKNAMECAPTURED", { opponentMon1 = "WURMPLE" }), function() end)
for _ = 1, 600 do
if Choice.isOpen() and Choice.style == "battle" then break end
Ui.pump()
U.wait(1)
end
result(Choice.isOpen() and Choice.style == "battle", "battle yes/no open")
U.wait(20)
U.tap(game, "down")
U.wait(10)
result(Choice.cursor == 2, "cursor on No")
local withBox = DIR .. "/s7_01_" .. version .. "_yesno_cursor_on_no.png"
local noBox = DIR .. "/s7_00_" .. version .. "_same_frame_without_yesno.png"
U.still(game, withBox)
local wasActive = Choice.active
Choice.active = false
U.still(game, noBox)
Choice.active = wasActive
local a = love.image.newImageData(love.filesystem.newFileData(assert(io.open(withBox, "rb")):read("*a"), "a.png"))
local b = love.image.newImageData(love.filesystem.newFileData(assert(io.open(noBox, "rb")):read("*a"), "b.png"))
local sx = math.floor(math.min(a:getWidth() / 240, a:getHeight() / 160))
local ox, oy = (a:getWidth() - 240 * sx) / 2, (a:getHeight() - 160 * sx) / 2
local minX, maxX, minY, maxY = 999, -1, 999, -1
for y = 56, 119 do
for x = 0, 239 do
local px, py = ox + math.floor((x + 0.5) * sx), oy + math.floor((y + 0.5) * sx)
local r1, g1, b1 = a:getPixel(px, py)
local r2, g2, b2 = b:getPixel(px, py)
if math.abs(r1 - r2) + math.abs(g1 - g2) + math.abs(b1 - b2) > 0.02 then
if x < minX then minX = x end
if x > maxX then maxX = x end
if y < minY then minY = y end
if y > maxY then maxY = y end
end
end
end
local wantLeft = (expect == "frlg") and 184 or 192
result(math.floor(minX / 8) == wantLeft / 8, string.format("frame left edge in tile %d (x=%d) got x=%d", wantLeft / 8, wantLeft, minX))
result(math.floor(maxX / 8) == 29, "frame right edge in tile 29 got x=" .. maxX)
result(math.floor(minY / 8) == 8 and math.floor(maxY / 8) == 13, string.format("frame rows 8..13 got y %d..%d", minY, maxY))
if expect == "rs" then
local r, g, bb = a:getPixel(ox + math.floor(204 * sx), oy + math.floor(88 * sx))
local r0, g0, b0 = a:getPixel(ox + math.floor(204 * sx), oy + math.floor(72 * sx))
print(string.format("rs cursor sample no-row %.2f,%.2f,%.2f yes-row %.2f,%.2f,%.2f", r, g, bb, r0, g0, b0))
end
Choice.reset()
Battle.abort("run")
U.wait(30)
finish()
end
+184
View File
@@ -0,0 +1,184 @@
local U = require("tests.drivers.util")
return function(game)
U.wait(20)
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
local App, path, tmp
local dim, safe, osName, getTime = love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS,
love.timer.getTime
local failures = 0
local function pass(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
end
local clock = 1000
love.timer.getTime = function() return clock end
local ok, err = xpcall(function()
App = require("tools.save-editor.App")
local Gen, Kit = require("Gen"), require("Kit")
local MapBrowser = require("MapBrowser")
local Serializer = require("src.core.SaveSerializer")
local version = require("src.core.GameVersion").get()
local dir = assert(os.getenv("POKEPORT_SHOT_DIR"))
tmp = os.tmpname()
path = tmp .. "-map-s8.lua"
local file = assert(io.open(path, "wb"))
file:write(Serializer.encode(Gen.newGame(version))); file:close()
App.load(path, { version = version, embedded = true })
local S = App.getState()
assert(S.save and not S.loadError and not S.missingCache, S.status)
local generation = Gen.ofState(S)
local maps = Gen.maps(S.data)
local mapId
for _, id in ipairs({ "PALLET_TOWN", "NEW_BARK_TOWN", "FR_PALLET_TOWN", "LITTLEROOT_TOWN" }) do
if maps[id] then mapId = id; break end
end
if not mapId then
local ids = {}
for id in pairs(maps) do if id:find("TOWN", 1, true) then ids[#ids + 1] = id end end
table.sort(ids)
mapId = assert(ids[1], "no town map")
end
local W, H, platform = 390, 844, "Android"
local audit
local function render(name)
love.graphics.getDimensions = function() return W, H end
love.window.getSafeArea = function() return 0, 0, W, H end
love.system.getOS = function() return platform end
local canvas = love.graphics.newCanvas(W, H, { msaa = 0 })
love.graphics.push("all")
love.graphics.setCanvas(canvas)
love.graphics.origin()
love.graphics.setScissor()
love.graphics.setShader()
require("src.render.GameViewport").reset()
love.graphics.clear(0, 0, 0, 1)
Kit.audit = {}
App.draw()
audit = Kit.audit
Kit.audit = nil
love.graphics.setCanvas()
love.graphics.pop()
if name then
local pixels = canvas:newImageData()
local bytes = pixels:encode("png"):getString()
pixels:release()
local out = assert(io.open(dir .. "/" .. version .. "-" .. name .. ".png", "wb"))
out:write(bytes); out:close()
end
canvas:release()
love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName
end
local function control(label)
for _, c in ipairs(audit or {}) do
if c.class == "control" and c.label == label then return c end
end
end
local function tap(x, y)
App.mousepressed(x, y, 1)
render()
end
local labels = { "Player", generation == 2 and "Spawn" or "Heal" }
if generation == 1 then labels[3] = "Outdoor" end
local function pickCell(map)
local r = S._mapViewRect
local hc = map.heightCells or map.height * 2
local wc = map.widthCells or map.width * 2
for cy = math.floor(hc * 0.75), 1, -1 do
for cx = 2, wc - 2 do
local sx = r.x + ((cx + 0.5) * 16 - S.mapCamX) * S.mapZoom
local sy = r.y + ((cy + 0.5) * 16 - S.mapCamY) * S.mapZoom
if not map:warpAtCell(cx, cy) and sx > r.x + 4 and sx < r.x + r.w - 4 and sy > r.y + 4
and sy < r.y + r.h - 4 and not MapBrowser.insideOverlay(S, sx, sy) then
return { cx = cx, cy = cy, x = sx, y = sy }
end
end
end
end
local startMap, startX, startY = Gen.playerMap(S.save)
local function view(tag, shotPrefix)
Gen.setPlayerHere(S.save, startMap, startX, startY)
S.save.lastHeal, S.save.lastOutdoor, S.save.spawn = nil, nil, nil
S.save.healMap, S.save.healX, S.save.healY = nil, nil, nil
S.tab, S.navPopup, S.editPopup = "map", nil, nil
MapBrowser.select(S, mapId)
render(); render()
for _, label in ipairs(labels) do
local c = control(label)
local r = S._mapViewRect
pass(c ~= nil and r ~= nil and c.x >= r.x and c.y >= r.y and c.x + c.w <= r.x + r.w and c.y + c.h <= r.y + r.h
and c.h >= Kit.tapMin(), version .. " " .. tag .. " " .. label .. " button sits on the map at tap size")
end
pass(control("Set here") == nil, version .. " " .. tag .. " has no separate Set here card")
clock = clock + 10
local first = control(labels[2])
tap(first.x + first.w / 2, first.y + first.h / 2)
clock = clock + 1
render(shotPrefix .. "_01_no_cell_reason")
pass(S.toast and S.toast.kind == "info" and S.toast.text:find("Tap a cell first", 1, true) ~= nil
and S.mapClickCell == nil, version .. " " .. tag .. " disabled button explains itself and selects nothing")
local _, map = MapBrowser.preview(S)
local pick = assert(pickCell(map), "no free cell")
tap(pick.x, pick.y)
pass(S.mapClickCell and S.mapClickCell.cx == pick.cx and S.mapClickCell.cy == pick.cy,
version .. " " .. tag .. " tap selects a map cell")
for i, label in ipairs(labels) do
render()
local c = control(label)
clock = clock + 10
tap(c.x + c.w / 2, c.y + c.h / 2)
clock = clock + 1
render(("%s_%02d_%s_toast"):format(shotPrefix, i + 1, label:lower()))
pass(S.toast and S.toast.kind == "ok", version .. " " .. tag .. " " .. label .. " shows a green toast: "
.. tostring(S.toast and S.toast.text))
pass(S.mapClickCell and S.mapClickCell.cx == pick.cx and S.mapClickCell.cy == pick.cy,
version .. " " .. tag .. " " .. label .. " tap does not fall through to the map")
end
return pick
end
local function verify(tag, pick)
assert(App.save(), "save failed")
local f = assert(io.open(path, "rb"))
local saved = assert(Serializer.decode(f:read("*a")))
f:close()
local pm, px, py = Gen.playerMap(saved)
pass(pm == mapId and px == pick.cx and py == pick.cy,
("%s %s exported player at %s (%d,%d)"):format(version, tag, tostring(pm), px or -1, py or -1))
if generation == 3 then
pass(saved.healMap == mapId and saved.healX == pick.cx and saved.healY == pick.cy,
("%s %s exported heal location %s (%s,%s)"):format(version, tag, tostring(saved.healMap),
tostring(saved.healX), tostring(saved.healY)))
elseif generation == 2 then
pass(saved.spawn == mapId, ("%s %s exported spawn %s"):format(version, tag, tostring(saved.spawn)))
else
local heal, out = saved.lastHeal, saved.lastOutdoor
pass(heal and heal.map == mapId and heal.x == pick.cx and heal.y == pick.cy,
("%s %s exported lastHeal %s"):format(version, tag, heal and heal.map or "nil"))
pass(out and out.id == mapId and out.x == pick.cx and out.y == pick.cy,
("%s %s exported lastOutdoor %s"):format(version, tag, out and out.id or "nil"))
end
end
S.mapSection, S.mapFocused = "view", false
verify("phone", view("phone", "s8_phone"))
S.mapFocused = true
view("phone focused", "s8_phone_focused")
S.mapFocused = false
W, H, platform = 1360, 860, "OS X"
verify("desktop", view("desktop", "s8_desktop"))
end, debug.traceback)
if App then App.unload() end
love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName
love.timer.getTime = getTime
if path then
os.remove(path)
for _, backup in ipairs(require("tests.fs_io").globPrefix(path .. ".bak-")) do os.remove(backup) end
end
if tmp then os.remove(tmp) end
if not ok then print("FAIL save_editor_map_points_s8: " .. tostring(err)) end
love.event.quit((ok and failures == 0) and 0 or 1)
end
@@ -205,7 +205,7 @@ return function(game)
shot("selected-events", 390, 844)
end
S.tab = "map"
for _, section in ipairs({ "maps", "view", "spawn" }) do
for _, section in ipairs({ "maps", "view" }) do
S.mapSection = section
shot("map-" .. section, 360, 640)
shot("map-" .. section .. "-landscape", 640, 360)
+172
View File
@@ -0,0 +1,172 @@
local U = require("tests.drivers.util")
return function(game)
U.wait(20)
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
local App, path, tmp
local dim, safe, osName, getTime = love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS,
love.timer.getTime
local failures = 0
local function pass(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
end
local clock = 1000
love.timer.getTime = function() return clock end
local ok, err = xpcall(function()
App = require("tools.save-editor.App")
local Gen, Ops, Kit = require("Gen"), require("Ops"), require("Kit")
local ItemPicker = require("ItemPicker")
local Serializer = require("src.core.SaveSerializer")
local version = require("src.core.GameVersion").get()
local dir = assert(os.getenv("POKEPORT_SHOT_DIR"))
tmp = os.tmpname()
path = tmp .. "-toast-s6.lua"
local file = assert(io.open(path, "wb"))
file:write(Serializer.encode(Gen.newGame(version))); file:close()
App.load(path, { version = version, embedded = true })
local S = App.getState()
assert(S.save and not S.loadError and not S.missingCache, S.status)
Ops.partyAdd(S)
Ops.selectParty(S, 1)
local W, H, platform = 390, 844, "Android"
local function render(draw, name)
love.graphics.getDimensions = function() return W, H end
love.window.getSafeArea = function() return 0, 0, W, H end
love.system.getOS = function() return platform end
local canvas = love.graphics.newCanvas(W, H, { msaa = 0 })
love.graphics.push("all")
love.graphics.setCanvas(canvas)
love.graphics.origin()
love.graphics.setScissor()
love.graphics.setShader()
require("src.render.GameViewport").reset()
love.graphics.clear(0, 0, 0, 1)
draw()
love.graphics.setCanvas()
love.graphics.pop()
if name then
local pixels = canvas:newImageData()
local bytes = pixels:encode("png"):getString()
pixels:release()
local out = assert(io.open(dir .. "/" .. version .. "-" .. name .. ".png", "wb"))
out:write(bytes); out:close()
end
canvas:release()
love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName
end
local function frame(name) render(App.draw, name) end
local function settle(name)
frame()
clock = clock + 1
frame(name)
end
local function placed(label)
local r = S.toast and S.toast.rect
local statusH = (Kit.desktop and 28 or 38) * Kit.scale
pass(r ~= nil and r[1] >= 0 and r[1] + r[3] <= W and r[2] >= 0 and r[2] + r[4] <= H - statusH,
version .. " " .. label .. " toast is on screen above the status bar")
end
S.tab, S.monSection, S.mobileInspector = "party", "main", false
Ops.setNickname(S, S.save.party[1], "TOASTY")
settle("s6_01_ok_phone")
pass(S.toast and S.toast.kind == "ok", version .. " phone nickname edit shows a green toast")
placed("phone ok")
while #S.save.party < 6 do Ops.partyAdd(S) end
clock = clock + 10
Ops.partyAdd(S)
settle("s6_02_error_phone")
pass(S.toast and S.toast.kind == "error" and S.toast.text == S.status,
version .. " phone party-full refusal shows a red toast")
placed("phone error")
clock = clock + 10
Ops.openItemPicker(S, Kit, "bag")
frame(); frame()
local item = assert(Ops.itemSearch(S, "potion")[1])
ItemPicker.commit(S, Kit, item)
settle("s6_03_ok_over_item_picker_phone")
pass(S.itemPicker ~= nil and S.toast and S.toast.kind == "ok",
version .. " item add toasts over the open picker")
placed("phone picker")
Ops.closeItemPicker(S, Kit)
clock = clock + 10
Ops.say(S, "Party is full (6/6)")
S.mobileInspector, S.monSection, S.inspectorScroll = true, "main", 0
Kit.focus = "mon-nickname"
frame()
clock = clock + 1
Kit.focus = "mon-nickname"
frame("s6_04_error_clear_of_focused_inspector_phone")
local r, fr = S.toast and S.toast.rect, Kit.focusRect
local rects, n = Kit.trackedControls()
local covered = 0
for i = 1, n do
local c = rects[i]
if r and c[1] < r[1] + r[3] and c[1] + c[3] > r[1] and c[2] < r[2] + r[4] and c[2] + c[4] > r[2] then
covered = covered + 1
end
end
pass(r ~= nil and fr ~= nil and n > 0 and covered == 0 and r[2] >= 0 and r[2] + r[4] <= H,
version .. " focused phone field keeps the toast clear of every control (" .. covered .. " covered)")
Kit.blur()
S.mobileInspector = false
frame()
r = S.toast.rect
local selected = S.selectedParty
App.mousepressed(r[1] + r[3] / 2, r[2] + r[4] / 2, 1)
frame()
pass(S.toast == nil and S.selectedParty == selected, version .. " tapping the toast dismisses it and nothing under it")
W, H, platform = 1360, 860, "OS X"
clock = clock + 10
Ops.arm(S, "party-remove-1", "Click Remove again to release slot 1")
settle("s6_05_warn_arm_desktop")
pass(S.toast and S.toast.kind == "warn", version .. " desktop arm shows a yellow toast")
placed("desktop warn")
clock = clock + 10
Ops.setNickname(S, S.save.party[1], "SHINY")
settle("s6_06_ok_desktop")
pass(S.toast and S.toast.kind == "ok", version .. " desktop edit shows a green toast")
placed("desktop ok")
clock = clock + 10
Ops.partyAdd(S)
settle("s6_07_error_desktop")
pass(S.toast and S.toast.kind == "error", version .. " desktop refusal shows a red toast")
placed("desktop error")
App.unload()
App = nil
local Importer = require("src.import.RomImporter")
local Store = require("src.box.Store")
local View = require("src.import.LauncherView")
local imp = Importer.new(function() end, { launcher = true })
local service = { state = Store.new() }
service.read = function(_, source) return { version = source.version, party = {}, boxes = {} }, nil, source.path end
imp.tab = "box"
imp._boxState = { service = service, sources = {
{ version = "red", label = "Red", path = "first", slotId = "slot1" } },
sourceIndex = 1, box = 1, pcBox = 1, query = "", sort = "slot", pcRows = {},
selectedBox = {}, selectedPC = {}, pageBox = 1, pagePC = 1, view = "box", multi = true }
W, H, platform = 1024, 768, "OS X"
local function launcher(name) render(function() View.draw(imp) end, name) end
launcher(); launcher()
imp._boxState.notice, imp._boxState.noticeKind = "That bag pocket is full.", "error"
launcher(); clock = clock + 1; launcher("s6_08_box_error_toast_desktop")
local box = imp._toasts and imp._toasts.box and imp._toasts.box.toast
pass(box ~= nil and box.kind == "error" and box.rect ~= nil, "BOX error toast still draws through BoxUI")
end, debug.traceback)
if App then App.unload() end
love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName
love.timer.getTime = getTime
if path then
os.remove(path)
for _, backup in ipairs(require("tests.fs_io").globPrefix(path .. ".bak-")) do os.remove(backup) end
end
if tmp then os.remove(tmp) end
if not ok then print("FAIL save_editor_toast_s6: " .. tostring(err)) end
love.event.quit((ok and failures == 0) and 0 or 1)
end
+11 -2
View File
@@ -3,7 +3,9 @@ return function(game)
U.wait(20)
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
local App, Paint, Kit, path, tmp, oldPaint
local dim, safe, osName
local dim, safe, osName, getTime = nil, nil, nil, love.timer.getTime
local clock = 1000
love.timer.getTime = function() return clock end
local failures = 0
local function pass(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
@@ -38,7 +40,9 @@ return function(game)
end
S.save.party = { mon }
Ops.selectParty(S, 1)
assert(Ops.setUnownForm(S, mon, g == 3 and 5 or 6), "seed form F")
local seed = g == 3 and 5 or 6
if Ops.unownForm(S, mon) == seed then assert(Ops.setUnownForm(S, mon, seed + 1), "seed form G") end
assert(Ops.setUnownForm(S, mon, seed), "seed form F")
S.dirty = false
local W, H, platform = 390, 844, "Android"
local function frame(name)
@@ -84,7 +88,9 @@ return function(game)
frame()
end
local Toast = require("src.ui.kit.Toast")
S.tab, S.monSection, S.mobileInspector = "party", "main", false
Toast.clear(S)
frame(); frame("2726_01_roster_unown_f_phone")
pass(letterName() == "F", version .. " roster seeded with UNOWN F")
@@ -109,6 +115,7 @@ return function(game)
pass(current() == target, version .. " chooser sets the form to " .. letterName())
pass(S.dirty, version .. " form edit marks the save dirty")
frame()
clock = clock + 1
frame("2726_04_form_set_" .. letterName():gsub("%?", "qmark") .. "_phone")
pass(History.undo(S) and letterName() == "F", version .. " undo restores F")
pass(History.redo(S) and current() == target, version .. " redo reapplies")
@@ -116,6 +123,7 @@ return function(game)
W, H, platform = 1100, 720, "Linux"
S.inspectorScroll = 0
Toast.clear(S)
frame(); frame()
for _ = 1, 40 do
r = controls["choice-form"]
@@ -136,6 +144,7 @@ return function(game)
if App then App.unload() end
if Paint and oldPaint then Paint.draw = oldPaint end
if dim then love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName end
love.timer.getTime = getTime
if path then
os.remove(path)
for _, backup in ipairs(require("tests.fs_io").globPrefix(path .. ".bak-")) do os.remove(backup) end
+1 -1
View File
@@ -38,7 +38,7 @@ check(differ > 100, "Emerald front rows differ from FRLG (" .. differ .. ")")
local BattleChrome = require("src.ui.game3.battle_chrome")
BattleChrome._manifest = manifest
check(BattleChrome.isRse(), "chrome layout is rse")
check(BattleChrome.layout() == "emerald", "chrome layout is emerald")
local msg = BattleChrome.window(BattleChrome.WIN.MSG)
eq(msg.x, 16, "B_WIN_MSG left")
eq(msg.y, 120, "B_WIN_MSG top")
+9 -4
View File
@@ -120,16 +120,21 @@ T.check(imp._boxPrompt ~= nil and not imp._boxPopup, "rename opens the existing
keyboardChoice(imp, "escape")
imp._boxState.notice, imp._boxState.noticeKind = "That bag pocket is full.", "error"; settle(imp)
T.eq(imp._boxToast and imp._boxToast.kind, "error", "a failed operation shows a red toast")
local function boxToast() return imp._toasts and imp._toasts.box and imp._toasts.box.toast end
T.eq(boxToast() and boxToast().kind, "error", "a failed operation shows a red toast")
T.eq(imp._boxState.notice, nil, "notices no longer print inline")
T.check(controls["box-toast"] ~= nil, "the toast is drawn and tappable")
local toastRect = controls["box-toast"]
draw(imp, { x = toastRect.x + toastRect.w / 2, y = toastRect.y + toastRect.h / 2 }); settle(imp)
T.eq(imp._boxToast, nil, "tapping the toast dismisses it")
T.eq(boxToast(), nil, "tapping the toast dismisses it")
imp._boxState.notice, imp._boxState.noticeKind = "Pokémon deposited.", "ok"; settle(imp)
T.eq(imp._boxToast.kind, "ok", "a success shows a green toast")
T.eq(boxToast().kind, "ok", "a success shows a green toast")
clock = clock + 10; settle(imp)
T.eq(imp._boxToast, nil, "a success toast times out")
T.eq(boxToast(), nil, "a success toast times out")
UI.toast(imp, "Export failed", "error", nil, "red"); settle(imp)
T.eq(controls["box-toast"], nil, "another tab's toast is not drawn on the box tab")
T.eq(UI.currentToast(imp), nil, "another tab's toast does not occlude the box tab")
imp._toasts.red = nil
local changes = 0
UI.open(imp, "Pick", { { id = 1, label = "One" } }, nil, function() changes = changes + 1 end)
@@ -0,0 +1,61 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local eq = T.eq
package.loaded["src.core.game3.rom_text"] = {
plain = function(key) if key == "gText_BattleYesNoChoice" then return "Yes\nNo" end return key end,
}
local log
package.loaded["src.ui.game3.window"] = {
template = function(l, t, w, h) return { left = l, top = t, width = w, height = h } end,
userFrame = function(tpl) log.frame = { tpl.left - 1, tpl.top - 1, tpl.left + tpl.width, tpl.top + tpl.height } end,
stdFrame = function() end,
cursorPx = function(x, y) log.arrow = { x, y } end,
printPx = function(s, x, y) log.text[#log.text + 1] = { s, x, y } end,
}
package.loaded["src.ui.game3.rs.menu_cursor"] = {
draw = function(x, y, w) log.rsCursor = { x, y, w } end,
}
local layout = "frlg"
package.loaded["src.ui.game3.battle_chrome"] = { layout = function() return layout end }
package.loaded["src.core.game3.runtime"] = { getSession = function() return { options = { frameType = 0 } } end }
local Choice = require("src.ui.game3.choice")
local function run(l, cursor)
layout = l
log = { text = {} }
Choice.yesNo(function() end, { left = 24, top = 9, style = "battle" })
Choice.cursor = cursor or 1
Choice.draw()
return log
end
local function rect(r) return table.concat(r or {}, ",") end
local fr = run("frlg")
eq(rect(fr.frame), "23,8,29,13", "frlg frame tiles 23..29 x 8..13")
eq(rect(fr.arrow), "192,72", "frlg arrow at tile 24 row 9")
eq(fr.text[1] and fr.text[1][2], 200, "frlg labels at tile 25")
eq(fr.text[1] and fr.text[1][3], 74, "frlg label y +2")
eq(fr.rsCursor, nil, "frlg has no RS cursor")
local em = run("emerald", 2)
eq(rect(em.frame), "24,8,29,13", "emerald frame tiles 24..29 x 8..13")
eq(rect(em.arrow), "200,88", "emerald arrow at tile 25 row 11")
eq(em.text[1] and em.text[1][2], 208, "emerald labels at tile 26")
eq(em.text[1] and em.text[1][3], 73, "emerald label y +1")
eq(em.rsCursor, nil, "emerald has no RS cursor")
local rs = run("rs", 2)
eq(rect(rs.frame), "24,8,29,13", "rs frame tiles 24..29 x 8..13")
eq(rs.arrow, nil, "rs draws no arrow glyph")
eq(rect(rs.rsCursor), "200,88,32", "rs highlight cursor at (200, 72+16*pos) width 32")
eq(rs.text[1] and rs.text[1][2], 200, "rs labels at tile 25")
eq(rs.text[2] and rs.text[2][3], 88, "rs second label at y 88")
T.finish("game3 battle yes/no frame per layout")
@@ -0,0 +1,99 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local eq = T.eq
love = love or require("tests.love_stub")
local noop = function() end
local family = "rse"
local mapType = 1
local forced = false
package.loaded["src.core.game3.pokemon"] = {
FRIENDSHIP_EVENT_WALKING = 0,
FRIENDSHIP_EVENT_FAINT_OUTSIDE_BATTLE = 1,
adjustFriendship = noop,
currentMapSec = function() return 0 end,
displayMonName = function(mon) return mon.name or "MON" end,
}
package.loaded["src.core.game3.rom_text"] = { box = function(key) return key end }
package.loaded["src.core.game3.field_semantics"] = {
var = function(_, name) return name end,
getVar = function() return 0 end,
setVar = noop,
}
package.loaded["src.core.game3.field_modules"] = { enabled = function() return false end }
package.loaded["src.core.game3.profile"] = {
family = function() return family end,
forSession = function() return { field = {} } end,
}
package.loaded["src.core.game3.capabilities"] = { gate = function() return false end }
package.loaded["src.core.game3.rs.rematch"] = { enabled = function() return false end }
package.loaded["src.core.game3.faraway_island"] = { updateStepCounter = noop }
package.loaded["src.core.game3.mystery_gift"] = { incrementNewsStepCounter = noop }
package.loaded["src.core.game3.rse.init"] = { call = noop, isRse = function() return family == "rse" end }
package.loaded["src.core.game3.player"] = { cellX = 0, cellY = 0 }
package.loaded["src.core.game3.forced_movement"] = {
isForced = function() return forced end,
isForcedMovementTile = function() return false end,
}
package.loaded["src.core.game3.map"] = {
currentDef = function() return { mapType = mapType } end,
}
package.loaded["src.core.game3.se_ids"] = { SE_FIELD_POISON = 1 }
package.loaded["src.core.game3.audio"] = { playSe = noop, fadeOutBgm = noop }
package.loaded["src.core.game3.special_scene_rse"] = { countSSTidalStep = function() return false end }
package.loaded["src.core.game3.safari"] = { takeStep = function() return false end }
package.loaded["src.core.game3.daycare"] = { step = function() return nil end }
local StepEvents = require("src.core.game3.step_events")
local function session(steps)
return {
vars = { poisonSteps = steps },
party = { { name = "TORCHIC", hp = 30, status = "PSN" } },
}
end
local function walk(s, n)
for _ = 1, n do
StepEvents.onStepTaken(s, {})
StepEvents.flush()
end
end
family, mapType, forced = "rse", 9, false
local s = session(3)
walk(s, 12)
eq(s.party[1].hp, 30, "rse secret base: poison never bites")
eq(s.vars.poisonSteps, 3, "rse secret base: counter is not advanced")
mapType = 8
s = session(3)
walk(s, 1)
eq(s.party[1].hp, 29, "rse indoor: poison bites on the step the counter wraps")
s = session(0)
walk(s, 4)
eq(s.party[1].hp, 29, "rse: one bite per 4 steps")
walk(s, 4)
eq(s.party[1].hp, 28, "rse: next bite 4 steps later")
forced = true
s = session(3)
walk(s, 6)
eq(s.party[1].hp, 30, "rse forced step: poison skipped")
eq(s.vars.poisonSteps, 3, "rse forced step: counter is not advanced")
forced = false
family, mapType = "frlg", 1
s = session(0)
walk(s, 4)
eq(s.party[1].hp, 30, "frlg: no bite after 4 steps")
eq(s.vars.poisonSteps, 4, "frlg: counter at 4")
walk(s, 1)
eq(s.party[1].hp, 29, "frlg: bite on the 5th step")
walk(s, 5)
eq(s.party[1].hp, 28, "frlg: next bite 5 steps later")
StepEvents.flush()
T.finish("game3_field_poison_step_gate")
@@ -29,11 +29,8 @@ package.loaded["src.ui.game3.chrome"] = {
local BattleChrome = require("src.ui.game3.battle_chrome")
local Ui = require("src.core.game3.battle.ui")
local nativeLayout = "rs"
package.loaded["src.core.game3.profile"] = {
forSession = function() return { font = { nativeLayout = nativeLayout } } end,
}
BattleChrome._manifest = { layout = "rse" }
local RS = { layout = "rse", assetLayout = "rs" }
local EMERALD = { layout = "rse" }
local function frames(mode)
drawn = {}
@@ -49,13 +46,13 @@ local function same(a, b)
return true
end
nativeLayout = "rs"
BattleChrome._manifest = RS
local mv = frames("moves")
eq(same(mv, { { 1, 15, 20, 4 }, { 23, 15, 6, 4 } }), true, "RS move menu frames follow menu_map.bin")
eq((mv[1] and (mv[1][1] + mv[1][3]) * 8) or -1, 168, "RS left move box content ends at x=168")
eq(same(frames("menu"), { { 18, 15, 11, 4 } }), true, "RS action menu frame follows menu_map.bin")
nativeLayout = nil
BattleChrome._manifest = EMERALD
eq(same(frames("moves"), { { 1, 15, 18, 4 }, { 21, 15, 8, 4 } }), true, "Emerald move menu frames unchanged")
eq(same(frames("menu"), { { 16, 15, 13, 4 } }), true, "Emerald action menu frame unchanged")
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local eq = T.eq
local gfx = setmetatable({}, { __index = function() return function() end end })
_G.love = _G.love or { graphics = gfx }
local ui = {}
package.loaded["src.core.game3.profile"] = {
forSession = function() return { ui = ui, family = "rse" } end,
family = function() return "rse" end,
}
local keys = {}
package.loaded["src.core.game3.rom_text"] = {
plain = function(key, ctx)
keys[#keys + 1] = key
return ctx.stringVars[1] .. " COINS"
end,
}
local Window = require("src.ui.game3.window")
local FrlgFont = require("src.ui.game3.frlg_font")
local CoinsBox = require("src.ui.game3.coins_box")
local tpl, printed
Window.stdFrame = function(t) tpl = t end
Window.printPx = function(text, x, y) printed = { text = text, x = x, y = y } end
FrlgFont.measure = function(s) return #tostring(s) * 6 end
local RsUi = require("src.core.game3.profiles.rs.ui")
ui = RsUi
CoinsBox.show(0, 0, 50)
CoinsBox.draw()
eq(tpl and tpl.left, 1, "RS showcoinsbox 0,0 frame corner is tile 0 (interior at 1)")
eq(tpl and tpl.top, 1, "RS showcoinsbox 0,0 frame top is tile 0 (interior at 1)")
eq(tpl and tpl.width, 8, "RS coins interior is 8 tiles wide (frame x..x+9)")
eq(tpl and tpl.height, 2, "RS coins interior is 2 tiles tall (frame y..y+3)")
eq(keys[#keys], "gOtherText_Coins2", "RS coins text is gOtherText_Coins2")
eq(printed and printed.x, 8 + 7 + 2 * 6, "RS coins digits padded to four columns")
eq(printed and printed.y, 8, "RS coins text on the interior's first row")
ui = {}
CoinsBox.show(1, 1, 50)
CoinsBox.draw()
eq(tpl and tpl.left, 1, "Emerald showcoinsbox 1,1 interior stays at the script origin")
eq(tpl and tpl.top, 1, "Emerald showcoinsbox 1,1 interior top stays at the script origin")
eq(keys[#keys], "gText_Coins", "Emerald coins text is gText_Coins")
T.finish("game3 RS coins box")
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local eq = T.eq
local function romTextPlain(key) return key end
package.loaded["src.core.game3.rom_text"] = {
plain = romTextPlain, box = romTextPlain, ascii = romTextPlain, has = function() return true end,
key = function(n) return n end, at = function(n) return n end,
count = function() return 0 end, list = function() return {} end,
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
}
local log
local saveMenu = "frlg"
package.loaded["src.ui.game3.window"] = {
OPTION_HEIGHT = 15, CURSOR_WIDTH = 8,
template = function(l, t, w, h) return { left = l, top = t, width = w, height = h, h = h } end,
stdFrame = function(tpl) log.std = { tpl.left, tpl.top, tpl.width, tpl.height } end,
userFrame = function() end,
cursorPx = function(x, y) log.arrow = { x, y } end,
printPx = function(s, x, y) log.text[#log.text + 1] = { s, x, y } end,
menuRowPx = function(top, i) return top + (i - 1) * 15 end,
}
package.loaded["src.ui.game3.rs.menu_cursor"] = {
draw = function(x, y, w) log.rsCursor = { x, y, w } end,
}
package.loaded["src.core.game3.runtime"] = { getSession = function() return { version = "sapphire" } end, isActive = function() return true end }
package.loaded["src.core.game3.profile"] = {
forSession = function() return { ui = { saveMenu = saveMenu } } end,
family = function() return "rse" end,
}
local Choice = require("src.ui.game3.choice")
local function run(layout, cursor, left, top)
saveMenu = layout
log = { text = {} }
Choice.yesNo(function() end, left and { left = left, top = top } or nil)
Choice.cursor = cursor or 1
Choice.draw()
return log
end
local function rect(r) return table.concat(r or {}, ",") end
local fr = run("frlg", 2, 20, 8)
eq(rect(fr.std), "21,9,6,4", "frlg yesnobox 20,8 ignores left/top (ScriptMenu_YesNo fixed template)")
eq(rect(fr.arrow), "168,88", "frlg yesnobox arrow glyph on row 2 at tile 21")
eq(fr.rsCursor, nil, "frlg yesnobox draws no RS bar")
local frd = run("frlg")
eq(rect(frd.std), "21,9,6,4", "frlg default Yes/No interior at (21,9)")
local em = run("rse", 2, 20, 8)
eq(rect(em.std), "21,9,5,4", "emerald yesnobox 20,8 uses sYesNo_WindowTemplates 21,9 5x4")
eq(rect(em.arrow), "168,88", "emerald yesnobox arrow glyph at tile 21")
local emd = run("rse", 1, 3, 2)
eq(rect(emd.std), "21,9,5,4", "emerald yesnobox 3,2 still at 21,9")
local function multi(layout, cursor, opts, cols, left, top)
saveMenu = layout
log = { text = {} }
package.loaded["src.ui.game3.frlg_font"] = { measure = function(s) return #s * 6 end }
Choice.multi(opts, cursor - 1, function() end, { left = left, top = top, cols = cols, maxRight = 29 })
Choice.draw()
package.loaded["src.ui.game3.frlg_font"] = nil
return log
end
local fm = multi("frlg", 1, { "50 COINS", "CANCEL" }, 1, 16, 1)
eq(fm.rsCursor, nil, "frlg multichoice draws no RS bar")
eq(fm.arrow ~= nil, true, "frlg multichoice keeps the arrow glyph")
local rm = multi("rs", 2, { "50 COINS", "500 COINS", "CANCEL" }, 1, 16, 1)
eq(rect(rm.std), "16,1,7,6", "rs multichoice 15,0: interior at 16,1, width = widest label in tiles, height 2*count")
eq(rm.arrow, nil, "rs multichoice draws no FRLG arrow glyph")
eq(rect(rm.rsCursor), "128,24,56", "rs multichoice highlight bar at ((left+1)*8, (top+1+2*pos)*8) width 8*tiles")
eq(rm.text[1] and rect(rm.text[1]), "50 COINS,128,8", "rs multichoice item 0 at tile left+1, top+1")
eq(rm.text[3] and rect(rm.text[3]), "CANCEL,128,40", "rs multichoice item 2 at top+5")
local rc = multi("rs", 1, { "LILYCOVE", "BATTLE TOWER", "CANCEL" }, 1, 20, 5)
eq(rect(rc.std), "20,5,9,6", "rs multichoice right <= 29 keeps its left")
local rk = multi("rs", 1, { "ABCDEFGHIJKLMNOPQRSTUVWX" }, 1, 20, 5)
eq(rect(rk.std), "11,5,18,2", "rs multichoice right > 29 shifts left (left += 29 - right)")
eq(rect(rk.rsCursor), "88,40,144", "rs multichoice shifted bar follows the frame")
local rg = multi("rs", 5, { "PSN", "PAR", "SLP", "BRN", "FRZ", "CANCEL" }, 3, 9, 2)
eq(rect(rg.std), "9,2,17,4", "rs multichoicegrid 8,1 x3: columns widest+1 apart, rows = count/cols")
eq(rg.arrow, nil, "rs multichoicegrid draws no FRLG arrow glyph")
eq(rect(rg.rsCursor), "120,32,40", "rs multichoicegrid bar at column 1 row 1, width = widest label")
eq(rg.text[5] and rect(rg.text[5]), "FRZ,120,32", "rs multichoicegrid item 4 at column 1 row 1")
local rs = run("rs", 2, 20, 8)
eq(rect(rs.std), "21,9,5,4", "rs yesnobox 20,8 frames tiles 20..26 x 8..13 (interior 21,9 5x4)")
eq(rs.arrow, nil, "rs yesnobox draws no FRLG arrow glyph")
eq(rect(rs.rsCursor), "168,88,40", "rs yesnobox highlight bar at (168, 72+16*pos) width 40")
eq(rs.text[1] and rect(rs.text[1]), "gText_Yes,168,72", "rs YES at tile 21 row 9")
eq(rs.text[2] and rect(rs.text[2]), "gText_No,168,88", "rs NO at tile 21 row 11")
local rsd = run("rs", 1)
eq(rect(rsd.std), "21,9,5,4", "rs default Yes/No is DisplayYesNoMenu(20, 8)")
eq(rect(rsd.rsCursor), "168,72,40", "rs default bar on YES")
package.loaded["src.ui.game3.choice"] = nil
local loveStub = require("tests.love_stub")
_G.love = _G.love or loveStub
local StartMenu = require("src.ui.game3.start_menu")
local FrlgFont = require("src.ui.game3.frlg_font")
local Chrome = require("src.ui.game3.chrome")
local RsData = require("src.ui.game3.rs.start_menu_data")
local RseData = require("src.ui.game3.rse.start_menu_data")
local ListMenu = require("src.ui.game3.list_menu")
FrlgFont.draw = function(s, x, y) log.text[#log.text + 1] = { s, x, y } end
Chrome.dialogueFrame = function() end
ListMenu.drawScrollArrows = function() end
local function confirm(dataMod, cursor)
log = { text = {} }
StartMenu.ENTRIES = { { id = "exit", label = "EXIT" } }
StartMenu._data = dataMod
StartMenu._ctx = nil
StartMenu.open = true
StartMenu.cursor = 1
StartMenu._scrollOffset = 0
StartMenu._confirmExit = true
StartMenu._confirmCursor = cursor
StartMenu.draw()
return log
end
local function lastText(l, s)
for _, t in ipairs(l.text) do if t[1] == s then return rect(t) end end
end
local em = confirm(RseData, 2)
eq(rect(em.std), "21,9,6,4", "emerald exit confirm keeps the FRLG template")
eq(rect(em.arrow), "169,90", "emerald exit confirm arrow on NO")
eq(em.rsCursor, nil, "emerald exit confirm draws no RS bar")
local rx = confirm(RsData, 2)
eq(rect(rx.std), "21,9,5,4", "rs exit confirm frames tiles 20..26 x 8..13")
eq(rx.arrow, nil, "rs exit confirm draws no FRLG arrow glyph")
eq(rect(rx.rsCursor), "168,88,40", "rs exit confirm highlight bar at (168, 88) width 40 on NO")
eq(lastText(rx, "gText_Yes"), "gText_Yes,168,72", "rs exit confirm YES at (168, 72)")
eq(lastText(rx, "gText_No"), "gText_No,168,88", "rs exit confirm NO at (168, 88)")
local ry = confirm(RsData, 1)
eq(rect(ry.rsCursor), "168,72,40", "rs exit confirm bar on YES")
T.finish("game3 rs field yes/no boxes use the RS highlight bar")
@@ -0,0 +1,62 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local eq, check = T.eq, T.check
local drawn = {}
package.loaded["src.ui.game3.chrome"] = {
_frameType = 0,
userFrame = function(_, x, y, w, h) drawn[#drawn + 1] = { x, y, w, h } end,
}
local BattleChrome = require("src.ui.game3.battle_chrome")
eq(BattleChrome.isRse, nil, "BattleChrome.isRse is gone")
BattleChrome._manifest = { layout = "rse", assetLayout = "rs" }
eq(BattleChrome.layout(), "rs", "RS battle manifest resolves to the rs layout")
eq(BattleChrome.isHoenn(), true, "RS is Hoenn")
local rects = BattleChrome.menuFrameRects("menu", BattleChrome.layout())
eq(rects and rects[1][1], 18, "RS action menu frame starts at tile 18")
drawn = {}
BattleChrome.drawMenuFrames("moves")
eq(#drawn == 2 and drawn[1][3] == 20 and drawn[2][1] == 23, true,
"drawMenuFrames reads the RS layout without a caller hint")
BattleChrome._manifest = { layout = "rse" }
eq(BattleChrome.layout(), "emerald", "Emerald battle manifest resolves to the emerald layout")
eq(BattleChrome.isHoenn(), true, "Emerald is Hoenn")
drawn = {}
BattleChrome.drawMenuFrames("menu")
eq(#drawn == 1 and drawn[1][1] == 16 and drawn[1][3] == 13, true, "Emerald action menu frame unchanged")
BattleChrome._manifest = {}
eq(BattleChrome.layout(), "frlg", "manifest without a layout is frlg")
eq(BattleChrome.isHoenn(), false, "FRLG is not Hoenn")
drawn = {}
BattleChrome.drawMenuFrames("menu")
eq(#drawn, 0, "FRLG draws no battle menu frames")
BattleChrome._manifest = nil
eq(BattleChrome.layout(), "frlg", "no manifest is frlg")
local RsUi = require("src.core.game3.profiles.rs.ui")
eq(RsUi.saveMenu, "rs", "RS save menu layout is rs")
eq(type(RsUi.coinsWindow) == "table" and RsUi.coinsWindow.frameOrigin, true,
"RS coins window draws its frame at the script origin")
local seen = {}
local function walk(t, path)
if seen[t] then return end
seen[t] = true
for k, v in pairs(t) do
local p = path .. "." .. tostring(k)
if v == "rse" then check(false, "RS ui profile value " .. p .. " is the Emerald layout") end
if type(v) == "table" then walk(v, p) end
end
end
walk(RsUi, "ui")
check(true, "RS ui profile walked for rse layout values")
T.finish("game3 RS layout keys")
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local eq = T.eq
local gfx = setmetatable({}, { __index = function() return function() end end })
_G.love = _G.love or { graphics = gfx }
local layout = "rs"
package.loaded["src.core.game3.profile"] = {
forSession = function() return { ui = { saveMenu = layout } } end,
family = function() return "rse" end,
}
package.loaded["src.core.game3.rom_text"] = {
plain = function(key) return key end,
}
local Window = require("src.ui.game3.window")
local Chrome = require("src.ui.game3.chrome")
local FrlgFont = require("src.ui.game3.frlg_font")
local SaveMenu = require("src.ui.game3.save_menu")
local frames, texts = {}, {}
local arrow, bar
Window.stdFrame = function(tpl) frames[#frames + 1] = tpl end
Chrome.dialogueFrame = function() end
Chrome.dialogueWindow = function() return 2, 15, 26 end
Window.cursorPx = function(x, y) arrow = { x, y } end
package.loaded["src.ui.game3.rs.menu_cursor"] = { draw = function(x, y, w) bar = { x, y, w } end }
package.loaded["src.core.game3.rse.init"] = { system = function() return nil end }
FrlgFont.measure = function(s) return #tostring(s) * 6 end
FrlgFont.wrap = function(s) return s end
FrlgFont.linePitch = function() return 16 end
FrlgFont.draw = function(s, x, y, opts) texts[#texts + 1] = { s = tostring(s), x = x, y = y, colors = opts and opts.colors } end
local hasDex = true
SaveMenu.hasDex = function() return hasDex end
SaveMenu.locationName = function() return "LITTLEROOT TOWN" end
SaveMenu.countBadges = function() return 3 end
SaveMenu.countDex = function() return 42 end
local phase = "saved"
local function render()
frames, texts, arrow, bar = {}, {}, nil, nil
SaveMenu.open = true
SaveMenu._phase = phase
SaveMenu._session = { name = "MAY", gender = 1, playtime = { hours = 5, minutes = 7 } }
SaveMenu._layout = SaveMenu.layout(SaveMenu._session)
SaveMenu._rse = SaveMenu.isHoenn(SaveMenu._session)
SaveMenu.draw()
local stats = frames[1]
return stats, texts
end
local function find(s)
for _, t in ipairs(texts) do if t.s == s then return t end end
end
layout = "rs"
local stats = render()
eq(stats and stats.left, 1, "RS stats window interior starts at tile 1")
eq(stats and stats.width, 12, "RS stats window is 12 tiles wide (frame tiles 0..13)")
eq(stats and stats.height, 10, "RS stats window with the dex is 10 tiles tall (frame tiles 0..11)")
local header = find("LITTLEROOT TOWN")
eq(header and header.x, 8, "RS map name at tile 1")
eq(header and header.colors, FrlgFont.COLOR.NORMAL, "RS map name in the default menu color")
local name = find("MAY")
eq(name and name.x + 18, 104, "RS player name right-aligned to tile 13")
eq(name and name.colors, FrlgFont.COLOR.NORMAL, "RS values are not gender tinted")
local time = find("5:07")
eq(time and time.y, 8 + 64, "RS play time row is the fifth two-tile row")
hasDex = false
stats = render()
eq(stats and stats.height, 8, "RS stats window without the dex is 8 tiles tall")
hasDex = true
-- pokeruby/src/start_menu.c:699, pokeruby/src/menu.c:609, :721, :750
phase = "confirm"
SaveMenu.cursor = 2
render()
eq(arrow, nil, "RS save Yes/No draws no FRLG arrow glyph")
eq(bar and table.concat(bar, ","), "168,88,40", "RS save Yes/No highlight bar at (168, 72 + 16 * pos) width 40")
local yes = find("gText_Yes")
eq(yes and yes.x .. "," .. yes.y, "168,72", "RS YES label at tile (21, 9)")
local no = find("gText_No")
eq(no and no.x .. "," .. no.y, "168,88", "RS NO label at tile (21, 11)")
SaveMenu.cursor = 1
render()
eq(bar and bar[2], 72, "RS highlight bar follows the cursor to YES")
SaveMenu.cursor = 1
phase = "saved"
layout = "rse"
stats = render()
eq(stats and stats.width, 14, "Emerald stats window keeps its 14-tile template")
local em = find("LITTLEROOT TOWN")
eq(em and em.colors ~= FrlgFont.COLOR.NORMAL, true, "Emerald map name keeps its green header")
phase = "confirm"
SaveMenu.cursor = 2
render()
eq(bar, nil, "Emerald save Yes/No draws no RS highlight bar")
eq(arrow and table.concat(arrow, ","), "168,89", "Emerald save Yes/No keeps the arrow glyph on NO")
phase = "saved"
T.finish("game3 RS save stats window")
@@ -99,6 +99,7 @@ StepEvents.flush()
started = {}
profileField = nil
local session = poisonedSession()
session.vars.poisonSteps = 4
StepEvents.onStepTaken(session, {})
eq(types(), "poison_faint,poison_white_out", "firered keeps the Lua faint + white out events")
eq(session.party[1].status, nil, "firered clears status at faint time")
+75
View File
@@ -0,0 +1,75 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
local Kit = require("src.ui.kit.Kit")
local UI = require("src.import.BoxUI")
local View = require("src.import.LauncherView")
local Importer = require("src.import.RomImporter")
local Transition = require("src.ui.kit.Transition")
love.graphics.setLineJoin = love.graphics.setLineJoin or function() end
love.graphics.newShader = love.graphics.newShader or function() return {} end
local clock = 100
love.timer.getTime = function() return clock end
local controls, texts, original = {}, {}, View.btn
View.btn = function(imp, x, y, w, h, id, label, opts)
controls[id] = { x = x, y = y, w = w, h = h, label = label, opts = opts }
return original(imp, x, y, w, h, id, label, opts)
end
local printText = love.graphics.print
love.graphics.print = function(value, ...)
texts[#texts + 1] = tostring(value); return printText(value, ...)
end
love.graphics.getDimensions = function() return 1024, 768 end
love.graphics.getPixelDimensions = function() return 1024, 768 end
local imp = Importer.new(function() end, { launcher = true })
imp.tab = "red"
Transition.reset(); Kit.blur()
local function draw(click)
clock = clock + .3; controls, texts = {}, {}
imp._clickPt = click; imp:draw()
local queue = imp._uiActions; imp._uiActions = {}
imp:runActions(queue or {})
end
local function printed(text)
for _, t in ipairs(texts) do if t:find(text, 1, true) then return true end end
return false
end
imp.saveNotice = imp.saveNotice or {}
imp.saveNotice.red = { ok = false, text = "Export failed" }
draw(); draw()
local store = imp._toasts and imp._toasts.red
T.eq(store and store.toast and store.toast.kind, "error", "a failed save-card result raises a red toast")
T.eq(imp.saveNotice.red, nil, "the save card no longer keeps the failure text inline")
T.check(controls["box-toast"] ~= nil, "the launcher save toast is drawn and tappable")
T.check(printed("Export failed"), "the toast prints the save-card text")
local r = controls["box-toast"]
draw({ x = r.x + r.w / 2, y = r.y + r.h / 2 }); draw()
T.eq(imp._toasts.red.toast, nil, "tapping the save toast dismisses it")
imp.saveNotice.red = { ok = true, text = "Exported to /tmp/x/red.sav", dir = "/tmp/x" }
draw(); draw()
T.eq(imp._toasts.red.toast and imp._toasts.red.toast.kind, "ok", "an export success raises a green toast")
T.eq(imp.saveNotice.red and imp.saveNotice.red.dir, "/tmp/x", "the export keeps its folder for the inline Open folder button")
T.eq(imp.saveNotice.red and imp.saveNotice.red.text, "", "the export text moves out of the card")
T.check(printed("Open folder"), "the Open folder button stays on the save card")
imp.saveNotice.red = { ok = true, kind = "info", text = "Pick where to save red.sav..." }
draw()
T.eq(imp._toasts.red.toast.kind, "info", "a neutral notice raises a blue toast")
UI.toast(imp, nil, nil, nil, "red")
imp.saveNotice.red = { ok = true, persistent = true, text = "Copy your Red .sav into: sdmc:/x" }
draw(); draw()
T.eq(imp._toasts.red.toast, nil, "persistent instructions raise no toast")
T.eq(imp.saveNotice.red and imp.saveNotice.red.text, "Copy your Red .sav into: sdmc:/x",
"persistent instructions stay inline")
imp.saveNotice.red = nil
imp.saveNotice.blue = { ok = false, text = "Could not open the save editor (boom)." }
draw()
T.eq(imp._toasts.red.toast and imp._toasts.red.toast.text, "Could not open the save editor (boom).",
"an editor refusal toasts on the tab the player is on")
View.btn, love.graphics.print = original, printText
T.finish()
+323
View File
@@ -0,0 +1,323 @@
-- luajit tests/engine/love_run_shell_matrix.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")
local FsIo = require("tests.fs_io")
local Version = require("src.core.Version")
local HostShell = require("src.core.HostShell")
local SHELLS = {
{ file = "v0.1.24", last = "v0.1.41", shell = 1, legacy = true },
{ file = "v0.1.42", last = "v0.2.24", shell = 1, legacy = true },
{ file = "v0.2.25", last = "v0.2.26", shell = 1, legacy = true },
{ file = "v0.2.27", last = "v0.2.36", shell = 1, legacy = true },
{ file = "v0.2.37", last = "v0.2.38", shell = 1, legacy = true },
{ file = "v0.2.39", last = "v0.2.42", shell = 1, legacy = true },
{ file = "v0.2.43", last = "v0.2.45", shell = 1, legacy = true },
{ file = "v0.2.46", last = "v0.2.50", shell = 1, legacy = true },
{ file = "v0.2.51", last = "v0.3.57", shell = 2, legacy = true },
{ file = "v0.3.58", last = "v0.3.58", shell = 2, legacy = true },
{ file = "shell3", shell = 3, shipped = { { first = "v0.3.59", last = "v0.3.59", declared = 2 } } },
}
local function readFile(path)
local f = io.open(path, "rb")
if not f then return nil end
local s = f:read("*a")
f:close()
return s
end
local function stripComments(src)
local out, i, n, q = {}, 1, #src, nil
while i <= n do
local c = src:sub(i, i)
if q then
out[#out + 1] = c
if c == "\\" then
out[#out + 1] = src:sub(i + 1, i + 1)
i = i + 2
else
if c == q then q = nil end
i = i + 1
end
elseif c == '"' or c == "'" then
q = c
out[#out + 1] = c
i = i + 1
elseif src:sub(i, i + 1) == "--" then
local eqs = src:match("^%-%-%[(=*)%[", i)
if eqs then
local _, e = src:find("]" .. eqs .. "]", i, true)
i = (e or n) + 1
else
local nl = src:find("\n", i, true)
i = nl or (n + 1)
end
else
out[#out + 1] = c
i = i + 1
end
end
return table.concat(out)
end
local function normalize(src)
return (stripComments(src):gsub("%s+", " "):gsub("^ ", ""):gsub(" $", ""))
end
local function loveRunOf(src)
return src and src:match("\nfunction love%.run%(%)\n.-\nend\n")
end
local mainRun = loveRunOf(readFile("main.lua"))
check(mainRun ~= nil, "main.lua defines function love.run()")
local current
local maxLegacy = 0
for _, fx in ipairs(SHELLS) do
fx.src = readFile("tests/data/love_run/" .. fx.file .. ".lua")
check(fx.src ~= nil, "fixture " .. fx.file .. " exists")
fx.norm = normalize(fx.src or "")
if fx.legacy and fx.shell > maxLegacy then maxLegacy = fx.shell end
if mainRun and fx.norm == normalize(mainRun) then current = fx end
end
check(current ~= nil,
"main.lua love.run matches a tests/data/love_run fixture (a changed love.run is a new shell)")
if current then
eq(Version.shell, current.shell, "Version.shell names the shell main.lua's love.run implements")
end
local seen = {}
local prev = maxLegacy
for _, fx in ipairs(SHELLS) do
if not fx.legacy then
check(fx.shell > prev, "shell " .. fx.shell .. " (" .. fx.file .. ") is newer than every earlier shell")
check(not seen[fx.shell], "shell " .. fx.shell .. " names one love.run only")
seen[fx.shell] = true
prev = math.max(prev, fx.shell)
for _, rel in ipairs(fx.shipped or {}) do
check(rel.declared < fx.shell and rel.declared <= maxLegacy,
fx.file .. " shipped in " .. rel.first .. " under shell " .. rel.declared
.. ": a misdeclared release may only reuse a legacy shell number")
end
end
end
for _, fx in ipairs(SHELLS) do
if fx.legacy and current and fx ~= current then
check(fx.norm ~= current.norm or fx.shell == current.shell,
fx.file .. " has the same love.run as main.lua under a different shell number")
end
end
local payloadFiles = FsIo.luaFilesUnder("src")
payloadFiles[#payloadFiles + 1] = "main.lua"
check(#payloadFiles > 100, "payload scan found the source tree")
local function callArg(src, open)
local depth, i, n, q = 0, open, #src, nil
while i <= n do
local c = src:sub(i, i)
if q then
if c == "\\" then i = i + 1 elseif c == q then q = nil end
elseif c == '"' or c == "'" then
q = c
elseif c == "(" then
depth = depth + 1
elseif c == ")" then
depth = depth - 1
if depth == 0 then return src:sub(open + 1, i - 1) end
end
i = i + 1
end
return nil
end
local quitArgs = { [""] = true }
local quitSites = 0
for _, path in ipairs(payloadFiles) do
local src = stripComments(readFile(path) or "")
local isHostShell = path:match("src/core/HostShell%.lua$") ~= nil
local pos = 1
while true do
local s, e = src:find("love%.event%.quit%(", pos)
if not s then break end
local raw = callArg(src, e)
check(raw ~= nil, path .. ": love.event.quit( call at offset " .. s .. " never closes")
pos = e + 1
local arg = (raw or ""):gsub("^%s+", ""):gsub("%s+$", "")
quitSites = quitSites + 1
if arg == "" or arg:match("^%d+$") then
quitArgs[arg] = true
else
check(isHostShell, path .. " sends love.event.quit(" .. arg .. ") outside HostShell.restart")
check(arg:match('^"[^"]*"$') ~= nil or arg:match("^'[^']*'$") ~= nil,
path .. " sends love.event.quit(" .. arg .. "): the shell contract needs a literal argument")
end
end
end
check(quitSites >= 4, "payload scan found the love.event.quit call sites (" .. quitSites .. ")")
local realExit, realOpen = os.exit, io.open
local EXITED = {}
local queue, exits = {}, {}
local sleeps = 0
local osName = "Android"
local fused = false
local function noop() end
local function installLove()
love.event = {
pump = noop,
poll = function()
return function()
local ev = table.remove(queue, 1)
if ev then return unpack(ev, 1, 7) end
end
end,
quit = function(...)
local n = select("#", ...)
queue[#queue + 1] = n == 0 and { "quit" } or { "quit", ... }
end,
push = function(name, ...) queue[#queue + 1] = { name, ... } end,
}
love.handlers = setmetatable({}, { __index = function() return noop end })
love.system = love.system or {}
love.system.getOS = function() return osName end
love.system.restartApp = nil
love.filesystem = love.filesystem or {}
love.filesystem.isFused = function() return fused end
local now = 0
love.timer = {
step = function() now = now + 1 / 60 return 1 / 60 end,
getTime = function() return now end,
sleep = function(s)
sleeps = sleeps + 1
if sleeps > 10000 then error("frame never ends: love.timer.sleep looped", 0) end
now = now + (s or 0)
end,
getDelta = function() return 1 / 60 end,
}
love.graphics = love.graphics or {}
love.graphics.isActive = function() return true end
love.graphics.origin = love.graphics.origin or noop
love.graphics.clear = love.graphics.clear or noop
love.graphics.getBackgroundColor = love.graphics.getBackgroundColor or function() return 0, 0, 0, 1 end
love.graphics.present = love.graphics.present or noop
love.window = love.window or {}
love.window.isVisible = function() return true end
love.window.hasFocus = function() return true end
love.load = noop
love.update = noop
love.draw = noop
love.quit = function() return false end
love.arg = love.arg or {}
love.arg.parseGameArguments = love.arg.parseGameArguments or function(a) return a or {} end
end
local function loadShell(fx)
local chunk, err = loadstring(fx.src, "=love_run/" .. fx.file)
if not chunk then return nil, err end
local env = setmetatable({
pacingEnabled = function() return true end,
checkEmergencyQuit = noop,
idlePresentationCap = function() return nil end,
}, { __index = _G, __newindex = _G })
setfenv(chunk, env)
local saved = love.run
local ok, e = pcall(chunk)
local run = love.run
love.run = saved
if not ok then return nil, e end
return run
end
local function step(stepper)
sleeps = 0
local ok, ret = pcall(stepper)
if not ok and ret == EXITED then return true, nil, true end
return ok, ret, false
end
local ACTIONS = {
{ name = "HostShell.restart", fn = function() HostShell.restart() end },
}
for arg in pairs(quitArgs) do
local n = tonumber(arg)
ACTIONS[#ACTIONS + 1] = {
name = "quit(" .. arg .. ")",
fn = function() if n then love.event.quit(n) else love.event.quit() end end,
}
end
table.sort(ACTIONS, function(a, b) return a.name < b.name end)
local OSES = { "Android", "iOS", "OS X", "Windows", "Linux" }
os.exit = function(code)
exits[#exits + 1] = code
if code ~= nil and type(code) ~= "number" and type(code) ~= "boolean" then
error("bad argument #1 to 'exit' (number expected, got " .. type(code) .. ")", 2)
end
error(EXITED, 0)
end
io.open = function(path, ...)
if path == "/proc/self/cmdline" then return nil end
return realOpen(path, ...)
end
local FrameCap = require("src.core.FrameCap")
local capWas = FrameCap.current
local runs = 0
for _, fx in ipairs(SHELLS) do
if fx.src and fx.shell >= (Version.minShell or 1) then
for _, os_ in ipairs(OSES) do
for _, act in ipairs(ACTIONS) do
local label = fx.file .. " shell on " .. os_ .. ", payload " .. act.name
osName, fused = os_, false
for i = #queue, 1, -1 do queue[i] = nil end
for i = #exits, 1, -1 do exits[i] = nil end
_G.POKEPORT_LOOP_RESTART = nil
_G.POKEPORT_LOOP_PANEL_SYNC = nil
HostShell.restarting = false
installLove()
FrameCap.current = capWas
local run, lerr = loadShell(fx)
check(run ~= nil, label .. ": fixture loads (" .. tostring(lerr) .. ")")
if run then
local okRun, stepper = pcall(run)
check(okRun and type(stepper) == "function", label .. ": love.run returns a stepper (" .. tostring(stepper) .. ")")
if okRun and type(stepper) == "function" then
local ok1, r1 = step(stepper)
check(ok1 and r1 == nil, label .. ": a default-cap frame runs on the payload's modules (" .. tostring(r1) .. ")")
FrameCap.applyOptions({ fpsCap = FrameCap.DISPLAY })
local ok2, r2 = step(stepper)
check(ok2 and r2 == nil, label .. ": a DISPLAY-option frame runs on the payload's modules (" .. tostring(r2) .. ")")
FrameCap.current = capWas
local okA, aerr = pcall(act.fn)
check(okA, label .. ": payload exit path runs (" .. tostring(aerr) .. ")")
local ok, ret, exited = step(stepper)
check(ok, label .. ": shell survives the payload's quit (" .. tostring(ret) .. ")")
if ok then
check(exited or ret ~= nil, label .. ": shell ends the frame loop")
check(exited or type(ret) == "number" or ret == "restart",
label .. ": shell hands LOVE a number or \"restart\", got " .. tostring(ret))
end
runs = runs + 1
end
end
end
end
end
end
FrameCap.current = capWas
os.exit, io.open = realExit, realOpen
check(runs >= #SHELLS * #OSES * 2, "every shell ran under every OS (" .. runs .. " runs)")
T.finish("love_run_shell_matrix")
@@ -0,0 +1,285 @@
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
love = require("tests.love_stub")
local T = require("tests.harness")
local bit = require("bit")
local Version = require("src.core.GameVersion")
local Serializer = require("src.core.SaveSerializer")
local K = require("tests.save_compat._codec")
local D = require("tests.save_compat._gen3_decode")
local B = require("tests.fixtures.save.bytes")
local G3 = require("tests.fixtures.save.gen3_build")
local G2 = require("tests.fixtures.save.gen2_build")
local Gen2Save = require("src.save_convert.Gen2Save")
local PokemonG3 = require("src.core.game3.pokemon")
local Unown = require("src.core.gen2.Unown")
local Mon2 = require("src.battle.gen2.Mon")
local Gen, Ops, MonOps = require("Gen"), require("Ops"), require("MonOps")
local State = require("State")
require("tests.game3_cache").mount()
local BEAD_MAIL = 127
local UNOWN_F = 0x00000105
local function g3mailState(version, fam)
Version.set(version)
local w = G3.base(version)
G3.setParty(w, { G3.mon({ species = 201, pid = UNOWN_F, tid = 0x1234, sid = 0x5678, item = BEAD_MAIL, mail = 0 }) })
local o = D.FULL[fam].mail
for k = 0, 8 do B.le(w.sb1, o + k * 2, 0xFFFF, 2) end
local name = G3.text("RED", 8)
for i = 1, 8 do w.sb1[o + 17 + i] = name[i] end
B.le(w.sb1, o + 26, 0x1234 + 0x5678 * 65536, 4)
B.le(w.sb1, o + 30, 30005, 2)
B.le(w.sb1, o + 32, BEAD_MAIL, 2)
local s = State.new()
s.save, s.version = assert(K.import(3, version, G3.emit(w))), version
s.save.pcItems = s.save.pcItems or {}
Gen.ensureBoxes(s.save)
return s
end
local function export3(s, v, fam)
local out = assert(K.export(3, v, Serializer.decode(Serializer.encode(s.save)), nil))
return out, assert(D.deep(out, fam))
end
for _, v in ipairs({ "firered", "emerald" }) do
local fam = v == "emerald" and "emerald" or "frlg"
local s = g3mailState(v, fam)
local mon = s.save.party[1]
T.eq(mon.mail, 0, v .. " fixture unown holds mail 0")
T.eq(s.save.mail[1].species, 30005, v .. " fixture mail shows UNOWN F")
T.eq(Ops.setUnownForm(s, mon, 25), true, v .. " form set to Z")
T.eq(s.save.mail[1].species, 30025, v .. " editor mail species follows the form")
local out, deep = export3(s, v, fam)
T.eq(deep.mail[1].species, 30025, v .. " exported mail species is UNOWN Z")
T.eq(deep.mail[1].itemId, BEAD_MAIL, v .. " exported mail keeps the item")
T.eq(deep.party[1].mail, 0, v .. " exported mon still points at mail 0")
T.eq(PokemonG3.unownLetter(deep.party[1].pid), 25, v .. " exported PID is Z")
local back = assert(K.import(3, v, out))
T.eq(back.mail[1].species, 30025, v .. " re-import reads mail species UNOWN Z")
s.data = s.data or {}
s.data.pokemon = s.data.pokemon or {}
s.data.pokemon[25] = { name = "PIKACHU", speciesId = 25 }
local ok = Ops.setSpecies(s, mon, 25)
if ok then
T.eq(s.save.mail[1].species, 25, v .. " species edit restamps mail species")
local _, deep2 = export3(s, v, fam)
T.eq(deep2.mail[1].species, 25, v .. " exported mail species follows a species edit")
else
print("[skip] " .. v .. " setSpecies needs species data: " .. tostring(s.status))
end
end
local UNOWN_DEF = {
id = "UNOWN", name = "UNOWN", dex = 201, index = 201, types = { "PSYCHIC" },
baseStats = { hp = 48, attack = 72, defense = 48, speed = 48, specialAttack = 72, specialDefense = 48 },
catchRate = 225, baseExp = 61, growthRate = "MEDIUM_FAST", genderRatio = 255,
}
local CYNDA_DEF = {
id = "CYNDAQUIL", name = "CYNDAQUIL", dex = 155, index = 155, types = { "FIRE" },
baseStats = { hp = 39, attack = 52, defense = 43, speed = 65, specialAttack = 60, specialDefense = 50 },
catchRate = 45, baseExp = 65, growthRate = "MEDIUM_SLOW", genderRatio = 31,
}
local g2data = {
pokemon = { UNOWN = UNOWN_DEF, CYNDAQUIL = CYNDA_DEF },
items = K.gen2Data.items, maps = K.gen2Data.maps,
moves = { HIDDEN_POWER = { id = "HIDDEN_POWER", name = "HIDDEN POWER", index = 237, pp = 15, power = 1, accuracy = 100, type = "NORMAL" } },
}
local function g2state(version, species, dvs)
Version.set(version)
local bytes = G2.build({ version = version, party = { G2.mon({ species = species, nick = "MON", dvs = dvs,
moves = { 237, 0, 0, 0 }, pp = { 15, 0, 0, 0 }, ppUps = { 0, 0, 0, 0 } }) } })
local s = State.new()
s.data = g2data
s.save, s.version = assert(Gen2Save.decode(bytes, version, g2data)), version
Gen.ensureBoxes(s.save)
return s
end
local function dvList(d) return { d.attack, d.defense, d.speed, d.special } end
local function dvsOf(t) return { attack = t[1], defense = t[2], speed = t[3], special = t[4] } end
local MAX_UNOWN = {
{ 9, 9, 11, 15 }, { 9, 9, 15, 15 }, { 9, 11, 13, 15 }, { 9, 11, 15, 15 }, { 9, 13, 15, 15 },
{ 9, 15, 13, 15 }, { 9, 15, 15, 15 }, { 11, 9, 15, 15 }, { 11, 11, 11, 15 }, { 11, 11, 15, 15 },
{ 11, 13, 13, 15 }, { 11, 13, 15, 15 }, { 11, 15, 15, 15 }, { 13, 9, 13, 15 }, { 13, 9, 15, 15 },
{ 13, 11, 15, 15 }, { 13, 13, 11, 15 }, { 13, 13, 15, 15 }, { 13, 15, 13, 15 }, { 13, 15, 15, 15 },
{ 15, 9, 15, 15 }, { 15, 11, 13, 15 }, { 15, 11, 15, 15 }, { 15, 13, 15, 15 }, { 15, 15, 11, 15 },
{ 15, 15, 15, 15 },
}
local function checkWritten(s, v, label, want)
local out = assert(Gen2Save.encode(s.save, v, nil, g2data))
local L = Gen2Save.layoutFor(v)
T.eq(Gen2Save.checksumValid(out, L), true, label .. " checksum valid")
local dvHi, dvLo = out:byte(L.wPartyMons + 0x15 + 1), out:byte(L.wPartyMons + 0x16 + 1)
T.eq(("%d/%d/%d/%d"):format(math.floor(dvHi / 16), dvHi % 16, math.floor(dvLo / 16), dvLo % 16),
table.concat(want, "/"), label .. " DV bytes at +0x15")
return assert(Gen2Save.decode(out, v, g2data))
end
for _, v in ipairs({ "gold", "crystal" }) do
for letter = 1, 26 do
for _, how in ipairs({ "maxDvs", "maxMon" }) do
local s = g2state(v, 201, dvList(Unown.dvsForLetter(letter) or MonOps.unownDvs(dvsOf({ 4, 6, 8, 2 }), letter)))
local mon = s.save.party[1]
T.eq(Unown.monLetter(mon), letter, v .. " fixture letter " .. letter)
if how == "maxMon" then
mon.statExp = mon.statExp or {}
Ops.maxMon(s, mon)
mon = s.save.party[1]
else
Ops.maxDvs(s, mon)
end
local label = ("%s %s letter %d"):format(v, how, letter)
T.eq(Unown.letterFromDVs(mon.dvs), letter, label .. " keeps the letter")
T.eq(table.concat(dvList(mon.dvs), "/"), table.concat(MAX_UNOWN[letter], "/"), label .. " max DVs for the letter")
T.eq(mon.dvs.hp, 15, label .. " HP DV 15")
local back = checkWritten(s, v, label, MAX_UNOWN[letter])
T.eq(Unown.monLetter(back.party[1]), letter, label .. " re-read letter")
end
end
for _, case in ipairs({ { 2, 10, 10, 10, 9, { 11, 10, 10, 10 } }, { 14, 10, 10, 10, 22, { 15, 10, 10, 10 } } }) do
for _, how in ipairs({ "maxDvs", "maxMon" }) do
local s = g2state(v, 201, { case[1], case[2], case[3], case[4] })
local mon = s.save.party[1]
T.eq(Unown.monLetter(mon), case[5], v .. " shiny fixture letter " .. case[5])
if how == "maxMon" then Ops.maxMon(s, mon) mon = s.save.party[1] else Ops.maxDvs(s, mon) end
local label = ("%s %s shiny unown %d"):format(v, how, case[5])
T.eq(Unown.letterFromDVs(mon.dvs), case[5], label .. " keeps the letter")
T.eq(mon.shiny, true, label .. " stays shiny")
T.eq(table.concat(dvList(mon.dvs), "/"), table.concat(case[6], "/"), label .. " shiny max DVs")
T.check(s.status:find("kept shiny", 1, true) ~= nil, label .. " status says kept shiny")
checkWritten(s, v, label, case[6])
end
end
for _, how in ipairs({ "maxDvs", "maxMon" }) do
local s = g2state(v, 155, { 6, 10, 10, 10 })
local mon = s.save.party[1]
T.eq(Mon2.vanillaShiny(mon.dvs), true, v .. " shiny cyndaquil fixture")
if how == "maxMon" then Ops.maxMon(s, mon) mon = s.save.party[1] else Ops.maxDvs(s, mon) end
local label = v .. " " .. how .. " shiny cyndaquil"
T.eq(mon.shiny, true, label .. " stays shiny")
T.eq(table.concat(dvList(mon.dvs), "/"), "15/10/10/10", label .. " highest shiny DVs")
T.check(s.status:find("kept shiny", 1, true) ~= nil, label .. " status says kept shiny")
local back = checkWritten(s, v, label, { 15, 10, 10, 10 })
T.eq(Mon2.vanillaShiny(back.party[1].dvs), true, label .. " re-read DVs are shiny")
local s2 = g2state(v, 155, { 3, 7, 1, 12 })
local m2 = s2.save.party[1]
if how == "maxMon" then Ops.maxMon(s2, m2) m2 = s2.save.party[1] else Ops.maxDvs(s2, m2) end
T.eq(table.concat(dvList(m2.dvs), "/"), "15/15/15/15", v .. " " .. how .. " plain cyndaquil gets 15s")
T.eq(m2.shiny, false, v .. " " .. how .. " plain cyndaquil not shiny")
T.check(s2.status:find("kept shiny", 1, true) == nil, v .. " " .. how .. " plain status has no shiny note")
end
end
local SPINDA = PokemonG3.SPECIES_SPINDA
local function bytesMoved(a, b)
local n = 0
for i = 0, 3 do
if bit.band(bit.rshift(a, 8 * i), 0xFF) ~= bit.band(bit.rshift(b, 8 * i), 0xFF) then n = n + 1 end
end
return n
end
local function isShiny(p, tsv) return bit.bxor(tsv, bit.bxor(math.floor(p / 65536), p % 65536)) < 8 end
local function genderOf(p) return (p % 256) < 127 and "F" or "M" end
do
local meta = PokemonG3.speciesMeta and PokemonG3.speciesMeta(SPINDA)
T.eq((meta and meta.genderRatio) or 127, 127, "spinda gender ratio is 127")
local rng = 308
local function rand(n)
rng = (rng * 1103515245 + 12345) % 2147483648
return math.floor(rng / 32768) % n
end
local one, plain, shinyTwo, shinyBases = 0, 0, 0, 0
local onTwo, onFour, broken, kept, offOne = 0, 0, 0, 0, 0
for _ = 1, 300 do
local tid, sid = rand(65536), rand(65536)
local tsv = bit.bxor(tid, sid)
local pid = rand(65536) * 65536 + rand(65536)
if pid == 0 then pid = 1 end
local nature, shiny, gender, ab = pid % 25, isShiny(pid, tsv), genderOf(pid), pid % 2
local function run(reqs)
reqs.basePid = pid
reqs.ability = reqs.ability or ab
return MonOps.generatePid(SPINDA, tid, sid, reqs)
end
local function ok(p, n, g, sh)
return p and p ~= 0 and p % 25 == n and genderOf(p) == g and isShiny(p, tsv) == sh and p % 2 == ab
end
if run({ nature = nature, gender = gender, shiny = shiny }) == pid then kept = kept + 1 end
local n2 = (nature + 1 + rand(24)) % 25
local p = run({ nature = n2, gender = gender, shiny = shiny })
if not ok(p, n2, gender, shiny) then broken = broken + 1 end
local function tally(q)
if not q then return end
if shiny then
if bytesMoved(pid, q) <= 2 then shinyTwo = shinyTwo + 1 end
elseif bytesMoved(pid, q) == 1 then
one = one + 1
end
end
if shiny then shinyBases = shinyBases + 1 else plain = plain + 1 end
tally(p)
local g2 = gender == "F" and "M" or "F"
p = run({ nature = nature, gender = g2, shiny = shiny })
if not ok(p, nature, g2, shiny) then broken = broken + 1 end
tally(p)
p = run({ nature = nature, gender = gender, shiny = not shiny })
if not ok(p, nature, gender, not shiny) then broken = broken + 1 end
if p then
local m = bytesMoved(pid, p)
if not shiny then
if m <= 2 then onTwo = onTwo + 1 end
if m == 4 then onFour = onFour + 1 end
end
end
local spid = bit.bxor(pid % 65536, tsv, rand(8)) * 65536 + pid % 65536
local sp = MonOps.generatePid(SPINDA, tid, sid, { nature = spid % 25, gender = genderOf(spid), shiny = false,
ability = spid % 2, basePid = spid })
if not (sp and sp % 25 == spid % 25 and genderOf(sp) == genderOf(spid) and not isShiny(sp, tsv)) then
broken = broken + 1
end
if sp and bytesMoved(spid, sp) == 1 then offOne = offOne + 1 end
end
T.eq(broken, 0, "spinda generatePid always meets nature, gender, ability and shininess")
T.eq(kept, 300, "spinda PID that already fits is returned unchanged")
T.check(plain >= 250, "spinda sample is mostly non-shiny")
T.eq(one, plain * 2, "spinda nature or gender edit moves exactly one spot")
T.eq(shinyTwo, shinyBases * 2, "spinda nature or gender edit on a shiny moves at most two spots")
T.eq(offOne, 300, "spinda shiny off moves exactly one spot")
T.eq(onFour, 0, "spinda shiny on never moves all four spots")
T.check(onTwo >= 150, ("spinda shiny on moves at most two spots in most cases (%d)"):format(onTwo))
end
do
Version.set("firered")
local w = G3.base("firered")
G3.setParty(w, { G3.mon({ species = SPINDA, pid = 0x8A3C5E71, tid = 0x1234, sid = 0x5678 }) })
local s = State.new()
s.save, s.version = assert(K.import(3, "firered", G3.emit(w))), "firered"
Gen.ensureBoxes(s.save)
local mon = s.save.party[1]
local before = mon.personality
MonOps.setNature(s.data, mon, (before % 25 + 5) % 25, 3)
T.eq(mon.personality % 25, (before % 25 + 5) % 25, "spinda setNature sets the nature")
T.eq(bytesMoved(before, mon.personality), 1, "spinda setNature moves one spot")
local out = assert(K.export(3, "firered", Serializer.decode(Serializer.encode(s.save)), nil))
local deep = assert(D.deep(out, "frlg"))
T.eq(deep.party[1].pid, mon.personality, "spinda PID round trips")
T.check(deep.party[1].checksumOk, "spinda substruct checksum valid")
local MonEditor = require("MonEditor")
local k1 = MonEditor.spriteKey(s, SPINDA, mon)
MonOps.setNature(s.data, mon, (mon.personality % 25 + 3) % 25, 3)
T.check(k1 ~= MonEditor.spriteKey(s, SPINDA, mon), "editor sprite key follows the spinda PID")
T.eq(MonEditor.spriteKey(s, 4, { personality = 1 }), 4, "non-spinda sprite key unchanged")
end
T.finish()
@@ -0,0 +1,191 @@
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
love = require("tests.love_stub")
local T = require("tests.harness")
local clock = 300
love.timer.getTime = function() return clock end
local App = require("tools.save-editor.App")
local Ops, Gen, Kit = require("Ops"), require("Gen"), require("Kit")
local MapBrowser = require("MapBrowser")
local SD = require("src.core.SaveData")
T.check(type(MapBrowser.pointActions) == "function", "the map browser builds its own point buttons")
if type(MapBrowser.pointActions) == "function" then
local function labels(actions)
local out = {}
for _, action in ipairs(actions) do out[#out + 1] = action.label end
return table.concat(out, ",")
end
local g2 = { save = { generation = 2, spawn = "SPAWN_HOME", position = { map = "NEW_BARK_TOWN", x = 5, y = 5 } },
mapId = "NEW_BARK_TOWN" }
local a2 = MapBrowser.pointActions(g2, nil)
T.eq(labels(a2), "Player,Spawn", "Gen 2 shows Player and Spawn")
T.eq(a2[2].run, Ops.setLastHeal, "Gen 2 Spawn writes through Ops.setLastHeal")
T.eq(a2[1].run, Ops.setPlayerHere, "Player writes through Ops.setPlayerHere")
local g3 = { save = { generation = 3, healMap = "FR_PALLET_TOWN", healX = 1, healY = 2 }, mapId = "FR_PALLET_TOWN" }
local a3 = MapBrowser.pointActions(g3, nil)
T.eq(labels(a3), "Player,Heal", "Gen 3 shows Player and Heal")
T.eq(a3[2].run, Ops.setLastHeal, "Gen 3 Heal writes through Ops.setLastHeal")
T.check(not a3[2].enabled and a3[2].reason:find("FR_PALLET_TOWN (1,2)", 1, true),
"with no cell the Gen 3 Heal button is off and says the current heal spot")
end
local path = os.tmpname() .. "-s8.lua"
local f = assert(io.open(path, "wb"))
f:write(SD.encode(Gen.newGame("red")))
f:close()
local loaded = pcall(App.load, path, { version = "red", embedded = true })
local S = App.getState()
if not loaded or not S or not S.save or S.missingCache or not (S.data.maps and S.data.maps.PALLET_TOWN) then
os.remove(path)
print("[skip] save_editor_map_point_buttons_s8: no red data")
T.finish("save_editor_map_point_buttons_s8")
return
end
local oldOS = love.system.getOS
local function frame(w, h, os, audit)
love.graphics.getDimensions = function() return w, h end
love.window.getSafeArea = function() return 0, 0, w, h end
love.system.getOS = function() return os end
clock = clock + .3
Kit.audit = audit and {} or nil
App.draw()
local got = Kit.audit
Kit.audit = nil
return got
end
local function controls(audit, label)
local out = {}
for _, c in ipairs(audit or {}) do
if c.class == "control" and c.label == label then out[#out + 1] = c end
end
return out
end
local function tap(x, y, shape)
App.mousepressed(x, y, 1)
frame(shape[1], shape[2], shape[3])
end
for _, shape in ipairs({ { 390, 844, "Android" }, { 1360, 860, "OS X" } }) do
local name = shape[3]
S.tab, S.mapFocused, S.navPopup, S.editPopup = "map", false, nil, nil
S.mapSection = "spawn"
S.save.lastHeal, S.save.lastOutdoor = nil, nil
MapBrowser.select(S, "PALLET_TOWN")
frame(shape[1], shape[2], shape[3])
if S._mapStacked then
T.eq(S.mapSection, "view", "a stale Spawn page falls back to the map view at " .. name)
end
local audit = frame(shape[1], shape[2], shape[3], true)
T.eq(#controls(audit, "Set here"), 0, "no separate Set here card at " .. name)
local r = S._mapViewRect
local buttons = {}
for _, label in ipairs({ "Player", "Heal", "Outdoor" }) do
local c = controls(audit, label)[1]
buttons[label] = c
T.check(c ~= nil, label .. " button is on the map at " .. name)
if c then
T.check(r and c.x >= r.x and c.y >= r.y and c.x + c.w <= r.x + r.w and c.y + c.h <= r.y + r.h,
label .. " sits inside the map viewport at " .. name)
T.check(c.h >= Kit.tapMin() and c.w >= Kit.tapMin(), label .. " meets the tap minimum at " .. name)
end
end
if buttons.Heal and r then
local before = SD.encode(S.save)
clock = clock + 10
S.mapClickCell = nil
tap(buttons.Heal.x + buttons.Heal.w / 2, buttons.Heal.y + buttons.Heal.h / 2, shape)
T.eq(S.toast and S.toast.kind, "info", "Heal with no cell explains itself at " .. name)
T.check(S.toast and S.toast.text:find("Tap a cell first", 1, true), "the reason names the missing cell at " .. name)
T.eq(S.mapClickCell, nil, "a tap on a disabled button does not select the cell under it at " .. name)
T.eq(SD.encode(S.save), before, "a disabled button writes nothing at " .. name)
local map = require("src.world.MapLoader").load(S.data, "PALLET_TOWN")
local pick
for cy = map.heightCells - 2, 1, -1 do
for cx = 1, map.widthCells - 2 do
local sx = r.x + ((cx + 0.5) * 16 - S.mapCamX) * S.mapZoom
local sy = r.y + ((cy + 0.5) * 16 - S.mapCamY) * S.mapZoom
if not pick and not map:warpAtCell(cx, cy) and sx > r.x + 2 and sx < r.x + r.w - 2
and sy > r.y + 2 and sy < r.y + r.h - 2 and not MapBrowser.insideOverlay(S, sx, sy) then
pick = { cx = cx, cy = cy, x = sx, y = sy }
end
end
end
T.check(pick ~= nil, "a free cell is visible at " .. name)
tap(pick.x, pick.y, shape)
T.check(S.mapClickCell and S.mapClickCell.cx == pick.cx and S.mapClickCell.cy == pick.cy,
"tapping the map selects a cell at " .. name)
local function press(label)
audit = frame(shape[1], shape[2], shape[3], true)
local c = controls(audit, label)[1]
clock = clock + 10
tap(c.x + c.w / 2, c.y + c.h / 2, shape)
T.check(S.mapClickCell and S.mapClickCell.cx == pick.cx and S.mapClickCell.cy == pick.cy,
label .. " tap does not reselect the cell under the button at " .. name)
T.eq(S.toast and S.toast.kind, "ok", label .. " raises a green toast at " .. name)
end
press("Heal")
local heal = S.save.lastHeal
T.check(heal and heal.map == "PALLET_TOWN" and heal.x == pick.cx and heal.y == pick.cy,
"Heal writes lastHeal for the selected cell at " .. name)
press("Player")
local pm, px, py = Gen.playerMap(S.save)
T.check(pm == "PALLET_TOWN" and px == pick.cx and py == pick.cy, "Player moves the player at " .. name)
press("Outdoor")
local out = S.save.lastOutdoor
T.check(out and out.id == "PALLET_TOWN" and out.x == pick.cx and out.y == pick.cy,
"Outdoor writes lastOutdoor for the selected cell at " .. name)
end
end
do
local shape = { 320, 568, "Android" }
S.tab, S.mapFocused, S.mapSection = "map", false, "view"
MapBrowser.select(S, "PALLET_TOWN")
frame(shape[1], shape[2], shape[3])
local audit = frame(shape[1], shape[2], shape[3], true)
T.eq(S._mapActionsCompact, true, "a viewport too narrow for the labels falls back to icon-only buttons")
local r = S._mapViewRect
for _, label in ipairs({ "Player", "Heal", "Outdoor" }) do
local c = controls(audit, label)[1]
T.check(c ~= nil, label .. " stays reachable as a compact button")
if c then
T.check(r and c.x >= r.x and c.y >= r.y and c.x + c.w <= r.x + r.w and c.y + c.h <= r.y + r.h,
label .. " compact button sits inside the map viewport")
T.check(c.h >= Kit.tapMin() and c.w >= Kit.tapMin() and c.w == c.h, label .. " compact button is a tap-size square")
end
end
local heal = controls(audit, "Heal")[1]
if heal and r then
S.mapClickCell = { cx = 3, cy = 3 }
clock = clock + 10
tap(heal.x + heal.w / 2, heal.y + heal.h / 2, shape)
T.eq(S.toast and S.toast.kind, "ok", "the compact Heal button writes the heal spot")
T.check(S.save.lastHeal and S.save.lastHeal.x == 3 and S.save.lastHeal.y == 3, "compact Heal writes the selected cell")
T.check(S.mapClickCell and S.mapClickCell.cx == 3, "the compact tap does not fall through to the map")
end
end
if S.data.maps.REDS_HOUSE_1F then
local shape = { 390, 844, "Android" }
MapBrowser.select(S, "REDS_HOUSE_1F")
S.save.visited = {}
S.mapClickCell = { cx = 2, cy = 2 }
local audit = frame(shape[1], shape[2], shape[3], true)
local c = controls(audit, "Outdoor")[1]
T.check(c ~= nil, "Outdoor is shown on an indoor map")
if c then
local before = SD.encode(S.save)
clock = clock + 10
tap(c.x + c.w / 2, c.y + c.h / 2, shape)
T.eq(S.toast and S.toast.kind, "error", "Outdoor on an indoor map is refused with a toast")
T.eq(SD.encode(S.save), before, "the refused Outdoor writes nothing")
end
end
love.system.getOS = oldOS
App.unload()
os.remove(path)
T.finish("save_editor_map_point_buttons_s8")
@@ -0,0 +1,58 @@
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
love = require("tests.love_stub")
local T = require("tests.harness")
local Ops = require("Ops")
local unown = { id = "UNOWN", name = "UNOWN", species = 201, speciesId = 201 }
local abra = { id = "ABRA", name = "ABRA", species = 63, speciesId = 63 }
local function state(generation, party)
return {
version = generation == 3 and "firered" or generation == 2 and "gold" or "red",
save = { generation = generation, party = party, player = { map = "X", x = 0, y = 0 } },
data = { pokemon = { [201] = unown, UNOWN = unown, [63] = abra, ABRA = abra,
MR_MIME = { id = "MR_MIME", name = "MR.MIME" }, NIDORAN_F = { id = "NIDORAN_F", name = "NIDORAN♀" } } },
mapId = "FR_PALLET_TOWN",
mapClickCell = { cx = 2, cy = 15 },
}
end
local S3 = state(3, { { species = 201, level = 25 }, { species = 63, level = 5 } })
Ops.selectParty(S3, 1)
T.eq(S3.note, "Selected party slot 1 (UNOWN)", "Gen 3 selection narrates the species name, not its number")
Ops.selectParty(S3, 2)
T.eq(S3.note, "Selected party slot 2 (ABRA)", "Gen 3 selection names ABRA, not 63")
T.eq(Ops.speciesName(S3, 201), "UNOWN", "speciesName resolves a Gen 3 species number")
T.eq(Ops.speciesName(S3, "MR_MIME"), "MR.MIME", "speciesName resolves a Gen 1 id to its display name")
T.eq(Ops.speciesName(S3, "NIDORAN_F"), "NIDORAN♀", "speciesName resolves the gendered Nidoran id")
T.eq(Ops.speciesName(S3, "NOT_A_MON"), "NOT_A_MON", "an unknown id falls back to itself")
Ops.partyMove(S3, -1)
T.eq(S3.status, "Moved ABRA to slot 1", "partyMove names the species")
T.eq(S3.toast and S3.toast.text, S3.status, "the move toast carries the same text")
local S1 = state(1, { { species = "MR_MIME", level = 5 } })
Ops.selectParty(S1, 1)
T.eq(S1.note, "Selected party slot 1 (MR.MIME)", "Gen 1 selection shows the display name")
T.check(Ops.setLastHeal(S3), "Gen 3 heal writes")
T.eq(S3.status, "Heal location set to FR_PALLET_TOWN (2,15)", "Gen 3 heal toast is plain words")
T.check(Ops.setPlayerHere(S3), "Gen 3 player writes")
T.eq(S3.status, "Player set to FR_PALLET_TOWN (2,15)", "player toast is plain words")
local S2 = state(2, {})
S2.mapId = "NEW_BARK_TOWN"
T.check(Ops.setLastHeal(S2), "Gen 2 spawn writes")
T.eq(S2.status, "Spawn point set to NEW_BARK_TOWN", "Gen 2 spawn toast is plain words")
S1.mapId, S1.save.visited = "PALLET_TOWN", { PALLET_TOWN = true }
T.check(Ops.setLastHeal(S1), "Gen 1 heal writes")
T.eq(S1.status, "Last heal set to PALLET_TOWN (2,15)", "Gen 1 heal toast is plain words")
local map = { id = "PALLET_TOWN", def = { tileset = "OVERWORLD", connections = {} } }
T.check(Ops.setLastOutdoor(S1, map), "Gen 1 outdoor writes")
T.eq(S1.status, "Last outdoor map set to PALLET_TOWN (2,15)", "Gen 1 outdoor toast is plain words")
for _, s in ipairs({ S1.status, S2.status, S3.status }) do
T.check(not s:find("%l%u"), "no internal field name leaks into: " .. s)
end
T.finish("save_editor_status_words_s6")
+222
View File
@@ -0,0 +1,222 @@
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
love = require("tests.love_stub")
local T = require("tests.harness")
local clock = 200
love.timer.getTime = function() return clock end
local App = require("tools.save-editor.App")
local Ops, Gen, Kit = require("Ops"), require("Gen"), require("Kit")
local SD = require("src.core.SaveData")
local path = os.tmpname() .. "-toast.lua"
local f = assert(io.open(path, "wb"))
f:write(SD.encode(Gen.newGame("red")))
f:close()
local loaded = pcall(App.load, path, { version = "red", embedded = true })
local S = App.getState()
if not loaded or not S or not S.save or S.missingCache then
os.remove(path)
print("[skip] save_editor_toast_s6: no red data")
os.exit(0)
end
T.check(S.toast and (S.toast.kind == "ok" or S.toast.kind == "warn" and S.status:find("quarantine", 1, true)),
"loading a save raises a green toast, yellow when the game would quarantine")
T.eq(S.toast and S.toast.text, S.status, "the load toast says what the status says")
while #S.save.party < 6 do Ops.partyAdd(S) end
clock = clock + 1
Ops.partyAdd(S)
T.eq(S.toast and S.toast.kind, "error", "a refusal raises a red toast")
T.eq(S.toast and S.toast.text, S.status, "the refusal toast carries the status text")
local mon = S.save.party[1]
clock = clock + 1
Ops.setNickname(S, mon, "TOASTY")
T.eq(S.toast and S.toast.kind, "ok", "a successful edit raises a green toast")
clock = clock + 1
Ops.setNickname(S, mon, "TOASTY")
T.eq(S.toast and S.toast.kind, "info", "a no-op edit raises a blue toast")
clock = clock + 1
Ops.arm(S, "party-remove-1", "Click Remove again to release slot 1")
T.eq(S.toast and S.toast.kind, "warn", "an armed confirm raises a yellow toast")
T.eq(S.toast and S.toast.hold, Ops.ARM_SECONDS, "the confirm toast lasts the arm window")
Ops.disarm(S)
T.eq(S.toast, nil, "disarming drops the confirm toast")
clock = clock + 1
Ops.say(S, "Party is full (6/6)")
local before = S.toast
Ops.selectParty(S, 2)
T.eq(S.toast, before, "selecting a slot does not toast")
T.check(S.note:find("Selected party slot 2", 1, true) ~= nil, "selection narration lands in the status bar note")
clock = clock + 1
Ops.mark(S, "Repeat edit")
local at = S.toast.at
clock = clock + .05
Ops.mark(S, "Repeat edit")
T.eq(S.toast.at, at, "a repeated toast does not slide in again")
clock = clock + 1
T.check(App.save(), "scratch save writes")
T.eq(S.toast and S.toast.kind, "ok", "saving raises a green toast")
T.check(S.toast and S.toast.text:find("Saved ", 1, true) == 1, "the save toast names the save")
T.eq(require("ItemPicker").feedback, nil, "the item picker has no private toast")
local printed = {}
local oldPrint, oldOS = love.graphics.print, love.system.getOS
love.graphics.print = function(text, ...)
printed[#printed + 1] = tostring(text)
return oldPrint(text, ...)
end
local function frame(w, h, os)
love.graphics.getDimensions = function() return w, h end
love.window.getSafeArea = function() return 0, 0, w, h end
love.system.getOS = function() return os end
printed = {}
clock = clock + .3
App.draw()
end
local function count(text)
local n = 0
for _, line in ipairs(printed) do if line == text then n = n + 1 end end
return n
end
for _, shape in ipairs({ { 390, 844, "Android" }, { 1360, 860, "OS X" } }) do
S.tab, S.mobileInspector = "party", false
clock = clock + 10
Ops.say(S, "Party is full (6/6)")
frame(shape[1], shape[2], shape[3])
frame(shape[1], shape[2], shape[3])
T.eq(count("Party is full (6/6)"), 1, "the result prints once, in the toast, at " .. shape[3])
local r = S.toast and S.toast.rect
T.check(r and r[2] + r[4] <= shape[2] and r[1] >= 0 and r[1] + r[3] <= shape[1],
"the toast sits on screen at " .. shape[3])
local selected, revision = S.selectedParty, S.revision
App.mousepressed(r[1] + r[3] / 2, r[2] + r[4] / 2, 1)
frame(shape[1], shape[2], shape[3])
T.eq(S.toast, nil, "tapping the toast dismisses it at " .. shape[3])
T.eq(S.selectedParty, selected, "the tap does not reach the row under the toast at " .. shape[3])
T.eq(S.revision, revision, "the tap does not edit anything at " .. shape[3])
end
clock = clock + 10
Ops.say(S, "Party is full (6/6)")
frame(390, 844, "Android")
local low = S.toast.rect[2]
Kit.focus = "mon-nickname"
frame(390, 844, "Android")
T.check(S.toast.rect[2] < 844 / 3 and S.toast.rect[2] < low, "a focused field on a phone moves the toast under the tab rail")
Kit.blur()
local MapBrowser, ItemPicker = require("MapBrowser"), require("ItemPicker")
local function auditFrame(shape)
love.graphics.getDimensions = function() return shape[1], shape[2] end
love.window.getSafeArea = function() return 0, 0, shape[1], shape[2] end
love.system.getOS = function() return shape[3] end
clock = clock + .3
Kit.audit = {}
App.draw()
local got = Kit.audit
Kit.audit = nil
return got
end
local function covered(audit, r)
local out = {}
for _, c in ipairs(audit) do
local x1, y1, x2, y2 = c.x, c.y, c.x + c.w, c.y + c.h
if c.clip then
x1, y1 = math.max(x1, c.clip.x), math.max(y1, c.clip.y)
x2, y2 = math.min(x2, c.clip.x + c.clip.w), math.min(y2, c.clip.y + c.clip.h)
end
if c.class == "control" and x1 < x2 and y1 < y2 and x1 < r[1] + r[3] and x2 > r[1]
and y1 < r[2] + r[4] and y2 > r[2] then
out[#out + 1] = c.label
end
end
return out
end
local function clear(name, shape)
local audit = auditFrame(shape)
local r = S.toast and S.toast.rect
T.check(r ~= nil, name .. ": the toast is drawn")
if not r then return end
local hits = covered(audit, r)
T.eq(#hits, 0, name .. ": no control sits under the toast [" .. table.concat(hits, ", ") .. "]")
T.check(r[1] >= 0 and r[1] + r[3] <= shape[1] and r[2] >= 0 and r[2] + r[4] <= shape[2],
name .. ": the toast stays on screen")
end
local phone, desktop = { 390, 844, "Android" }, { 1360, 860, "OS X" }
S.tab = "items"
Ops.openItemPicker(S, Kit, "bag")
auditFrame(phone); auditFrame(phone)
local item = assert(Ops.itemSearch(S, "potion")[1])
clock = clock + 10
ItemPicker.commit(S, Kit, item)
T.eq(S.toast and S.toast.kind, "ok", "the picker add toasts")
clear("phone item picker after add", phone)
local picker = {}
for _, c in ipairs(auditFrame(phone)) do picker[c.label] = c end
T.check(picker["item-picker"] and picker.Bag and picker.PC, "the picker field and chips are still drawn")
local hdr = S.toastHeader
T.check(hdr and S.toast.rect[2] < picker.Bag.y and hdr.y <= S.toast.rect[2] + S.toast.rect[4],
"the picker toast sits on the popup header row above the chips")
clear("desktop item picker after add", desktop)
Ops.closeItemPicker(S, Kit)
S.tab, S.mapSection, S.mapFocused = "map", "view", false
MapBrowser.select(S, "PALLET_TOWN")
auditFrame(phone); auditFrame(phone)
S.mapClickCell = { cx = 5, cy = 5 }
clock = clock + 10
Ops.setLastHeal(S)
T.eq(S.toast and S.toast.kind, "ok", "Heal toasts on the phone map")
clear("phone map after Heal", phone)
auditFrame(desktop); auditFrame(desktop)
clock = clock + 10
Ops.setLastHeal(S)
clear("desktop map after Heal", desktop)
S.tab, S.mapSection = "map", "maps"
auditFrame(phone); auditFrame(phone)
clock = clock + 10
Ops.say(S, "Jumped to PALLET_TOWN (5,5)", "ok")
clear("phone map list after a jump", phone)
S.tab, S.mobileInspector = "party", false
Ops.selectParty(S, 1)
auditFrame(phone); auditFrame(phone)
clock = clock + 10
Ops.setNickname(S, S.save.party[1], "TOASTIER")
clear("phone party after nickname edit", phone)
S.mobileInspector, S.monSection, S.inspectorScroll = true, "main", 0
auditFrame(phone); auditFrame(phone)
clock = clock + 10
Ops.say(S, "Party is full (6/6)")
Kit.focus = "mon-nickname"
clear("phone focused nickname field", phone)
T.check(Kit.focusRect ~= nil, "the nickname field is drawn while focused")
Kit.blur()
S.mobileInspector = false
auditFrame(desktop); auditFrame(desktop)
clock = clock + 10
Ops.arm(S, "party-remove", "Click Remove again to release slot 1")
T.eq(S.toast and S.toast.kind, "warn", "arming Remove toasts on the desktop")
local remove
for _, c in ipairs(auditFrame(desktop)) do if c.label == "Confirm?" or c.label == "Remove" then remove = c end end
T.check(remove ~= nil, "the Remove button is drawn while armed")
if remove then
local r = S.toast.rect
T.check(r[2] + r[4] <= remove.y or r[2] >= remove.y + remove.h or r[1] >= remove.x + remove.w
or r[1] + r[3] <= remove.x, "the arm toast does not overlap the Remove button it names")
end
Ops.disarm(S)
love.graphics.print, love.system.getOS = oldPrint, oldOS
os.remove(path)
T.finish("save_editor_toast_s6")
+1 -1
View File
@@ -147,7 +147,7 @@ c = drawYesNo({ left = 21, top = 9 }, true)
for _, e in ipairs(c) do if e[1] == "cursor" then cur = e end end
check(cur and cur[3] == 88, "cursor on NO sits one 14 px row down")
c = drawYesNo({ left = 20, top = 8 })
check(c[1][2] == 20 and c[1][3] == 8 and c[1][4] == 6, "explicit left/top still honored with the 6-tile width")
check(c[1][2] == 21 and c[1][3] == 9 and c[1][4] == 6, "explicit left/top ignored: ScriptMenu_YesNo fixed template 21,9 6x4")
print(("game3_fullscreen_display_test: %s (%d failed)"):format(failed == 0 and "PASS" or "FAIL", failed))
if failed > 0 then os.exit(1) end
+1 -1
View File
@@ -89,7 +89,7 @@ Choice.yesNo(function() end, { left = 22, top = 8 })
Choice.draw()
Choice.active = false
local yn = frames[#frames]
check(yn ~= nil and yn[1] == 22, "yes/no after multichoice keeps its own left (22)")
check(yn ~= nil and yn[1] == 21, "yes/no after multichoice ignores left 22 (fixed template 21,9)")
for i = #frames, 1, -1 do frames[i] = nil end
Choice.multi({ "A", "B" }, 0, function() end, { left = 14, top = 2 })
+11
View File
@@ -858,3 +858,14 @@ tests/save_compat/gen3_cache_selection_test.lua
tests/save_compat/regions_test.lua
tests/save_editor_split_stack_bug2671_test.lua
tests/save_editor_task6_tests.lua
tests/engine/game3_field_poison_step_gate_test.lua
tests/engine/game3_rs_layout_keys_test.lua
tests/engine/game3_rs_save_window_test.lua
tests/engine/game3_rs_coins_box_test.lua
tests/engine/game3_battle_yesno_frame_s7.lua
tests/engine/game3_rs_field_yesno_s2.lua
tests/engine/save_editor_mail_maxdv_spinda.lua
tests/engine/save_editor_toast_s6.lua
tests/engine/launcher_save_toast_s6.lua
tests/engine/save_editor_map_point_buttons_s8.lua
tests/engine/love_run_shell_matrix.lua
+1
View File
@@ -478,6 +478,7 @@ do
-- Add to bag
Ops.addToBag(S, "POTION")
checkEq(save.inventory["13"], 1, "POTION added to bag x1")
checkEq(S.status, "Added POTION x1 to the bag", "the Gen 3 add toast says where the item went")
check(save.bag ~= nil and save.bag.stacks ~= nil, "save.bag updated")
-- Adjust bag quantity
+8 -10
View File
@@ -293,11 +293,12 @@ for _, size in ipairs({
end
end
S.tab = "map"
for _, section in ipairs({ "maps", "view", "spawn" }) do
for _, section in ipairs({ "maps", "view" }) do
S.mapSection = section
frame("map " .. section, W, H)
S.mapSpawnScroll = 10000
frame("map scrolled " .. section, W, H)
S.mapClickCell = { cx = 1, cy = 1 }
frame("map selected " .. section, W, H)
S.mapClickCell = nil
end
S.mapSection, S.mapFocused = "view", true
frame("focused landscape map", W, H)
@@ -357,7 +358,7 @@ for _, size in ipairs({
{ "items", "itemView", 4 },
{ "boxes", "boxView", 2 },
{ "events", "eventsTab", 4 },
{ "map", "mapSection", 3 },
{ "map", "mapSection", 2 },
}) do
S.tab, S.monSection, S.itemView, S.boxView, S.mapSection =
case[1], "main", "bag", "storage", "view"
@@ -810,13 +811,10 @@ App.touchpressed("map-cell", x, y)
App.touchreleased("map-cell", x, y)
App.draw()
check(S.mapClickCell and S.mapClickCell.cx == cx and S.mapClickCell.cy == cy, "tap selects the intended map cell")
S.mapSection = "spawn"
App.draw()
tapMapControl("Set here")
tapMapControl("Player")
local playerMap, px, py = require("Gen").playerMap(S.save)
check(playerMap == mapId and px == cx and py == cy, "mobile spawn control uses the selected cell")
S.mapSection = "view"
App.draw()
check(playerMap == mapId and px == cx and py == cy, "mobile Player button on the map uses the selected cell")
check(S.mapClickCell and S.mapClickCell.cx == cx and S.mapClickCell.cy == cy, "tapping the map button does not reselect the cell under it")
tapMapControl("Back to editor")
check(not S.mapFocused and S._mapViewH == ordinaryHeight, "return restores the regular editor layout")
tapMapControl("Focus map")
+1 -1
View File
@@ -129,7 +129,7 @@ do
eq(S.mapClickCell, nil, "select drops the previous cell selection")
eq(S._mapCenteredFor, nil,
"select defers centring to the next draw, which knows the viewport size")
check(S.status:match("VIRIDIAN_CITY") ~= nil, "select says what it is showing")
check(S.note:match("VIRIDIAN_CITY") ~= nil, "select says what it is showing")
end
do
+15 -7
View File
@@ -316,8 +316,6 @@ check(App.save(), "bulk-edited save writes")
check(App.reload(), "bulk-edited save reloads")
S = App.getState()
check(require("Legality").save(S).errors == 0, "bulk edits survive save round trip")
-- Result clicks and keyboard adds report inside the open picker, where the
-- dimmed status bar cannot provide visible confirmation.
local picker = require("ItemPicker")
local item = assert(Ops.itemSearch(S, "potion")[1])
local oldClock, oldPrint = love.timer.getTime, love.graphics.print
@@ -331,27 +329,37 @@ local function pickerFrame()
printed = {}
App.draw()
end
local function shown(text)
for line in pairs(printed) do
if #line >= 8 and text:sub(1, #line) == line then return true end
end
return false
end
S.tab = "items"
Ops.openItemPicker(S, Kit, "bag")
pickerFrame()
pickerFrame()
check(picker.feedback == nil and picker.drawFeedback == nil, "picker has no private toast")
check(picker.commit(S, Kit, item), "bag add succeeds")
pickerFrame()
check(printed["Added to Bag"] and S.itemPicker ~= nil, "bag toast renders without closing picker")
toastClock = toastClock + 2.6
local added = S.toast and S.toast.text
check(S.toast and S.toast.kind == "ok", "bag add raises a green toast")
check(added and shown(added) and S.itemPicker ~= nil, "bag toast renders over the open picker")
toastClock = toastClock + 3.5 + 0.25 + 0.01
pickerFrame()
check(not printed["Added to Bag"], "toast disappears automatically")
check(not shown(added) and S.toast == nil, "toast disappears automatically")
S.itemPicker.dest = "pc"
App.textinput("potion")
App.keypressed("return")
pickerFrame()
check(printed["Added to PC"], "keyboard add renders PC toast")
check(S.toast and S.toast.kind == "ok" and S.toast.text:find("PC", 1, true) and shown(S.toast.text),
"keyboard add renders PC toast")
local realAdd = Ops.addToBag
Ops.addToBag = function(state) return Ops.say(state, "Bag is full") end
S.itemPicker.dest = "bag"
check(not picker.commit(S, Kit, item), "refused add does not report success")
pickerFrame()
check(printed["Item not added"] and not printed["Added to Bag"], "refused add has visible failure feedback")
check(S.toast and S.toast.kind == "error" and printed["Bag is full"], "refused add has visible failure feedback")
Ops.addToBag = realAdd
Ops.closeItemPicker(S, Kit)
love.timer.getTime, love.graphics.print = oldClock, oldPrint
+74 -20
View File
@@ -26,7 +26,32 @@ local Gen = require("Gen")
local PadInput = require("PadInput")
local Motion = require("Motion")
local Chooser = require("Chooser")
local Toast = require("src.ui.kit.Toast")
local PAL = Theme.PAL
local toastArea = {}
local toastSlots = {}
local function placeClearOfControls(x, w, h)
local rects, n = Kit.trackedControls()
local gap = 12 * toastSlots.s
local best, bestHits, bestAbove
local function try(y, fromAbove, limit)
if y < toastSlots.minY or y + h > (limit or toastSlots.maxY) then return end
local hits = 0
for i = 1, n do
local r = rects[i]
if r[1] < x + w and r[1] + r[3] > x and r[2] < y + h and r[2] + r[4] > y then hits = hits + 1 end
end
if not best or hits < bestHits then best, bestHits, bestAbove = y, hits, fromAbove end
end
local fr = toastSlots.field
try(fr.y + fr.h + gap, true)
try(fr.y - h - gap, false)
try(toastSlots.contentY + gap, true)
try(toastSlots.maxY - h - gap, false)
try(toastSlots.screenBottom - h - 4 * toastSlots.s, false, toastSlots.screenBottom)
return best, bestAbove
end
local Party = require("Party")
local Boxes = require("Boxes")
@@ -100,6 +125,7 @@ local function applyLoaded(path, statusVerb)
S.path = path
local existed = fileExists(path)
local save, err = SaveIO.load(path)
local kind = "ok"
if save then
S.save = save
S.status = statusVerb .. " " .. path
@@ -112,11 +138,13 @@ local function applyLoaded(path, statusVerb)
.. " ("
.. tostring(err)
.. "), Save disabled, use Reload after fixing the file"
kind = "error"
S.loadError = true
S.allowSave = false
else
S.save = Gen.newGame(S.version)
S.status = "No save at " .. path .. " (" .. tostring(err) .. "), editing new game stub"
kind = "info"
S.loadError = false
S.allowSave = true
end
@@ -150,6 +178,7 @@ local function applyLoaded(path, statusVerb)
if not prepared then
S.loadError, S.allowSave = true, false
S.status = "Save disabled: " .. tostring(prepareError)
Toast.show(S, S.status, "error", { sticky = true })
return
end
local probe = require("src.mods.Merge").deepCopy(S.save)
@@ -172,7 +201,9 @@ local function applyLoaded(path, statusVerb)
#S.validation.remappedMaps
)
end
if kind == "ok" and S.status:find("game would quarantine", 1, true) then kind = "warn" end
end
Toast.show(S, S.status, kind, { sticky = kind == "error" })
end
-- pathOverride lets tests point App.load at a scratch file instead of the
@@ -262,7 +293,7 @@ function App.openPath(path, force)
end
if S.dirty and not force and not S._openArmed then
S._openArmed = true
S.status = "Unsaved changes, open again to discard and load " .. path
Ops.say(S, "Unsaved changes, open again to discard and load " .. path, "warn")
return false
end
applyLoaded(path, "Opened")
@@ -276,7 +307,7 @@ function App.chooseAndOpen()
else
local osName = love and love.system and love.system.getOS and love.system.getOS()
if osName ~= "OS X" and osName ~= "Windows" and osName ~= "Linux" then
S.status = "File picker unavailable, drop a save.lua onto the window"
Ops.say(S, "File picker unavailable, drop a save.lua onto the window")
end
end
end
@@ -287,7 +318,7 @@ function App.filedropped(file)
end
local path = file.getFilename and file:getFilename() or nil
if not path or path == "" then
S.status = "Could not read dropped file path"
Ops.say(S, "Could not read dropped file path")
return
end
App.openPath(path)
@@ -336,7 +367,7 @@ local function cycleTab(delta)
end
idx = ((idx - 1 + delta) % #TABS) + 1
Motion.change(S, "tab", TABS[idx].id, delta)
Ops.say(S, "Tab: " .. TABS[idx].label)
Ops.note(S, "Tab: " .. TABS[idx].label)
end
-- Pad / Joy-Con actions from PadInput.gamepadpressed (A/B via GamepadMap so
@@ -386,7 +417,7 @@ function App.save()
if Gen.ofState(S) == 3 then
local prepared, result = pcall(require("Game3Adapter").export, S.save)
if not prepared then
S.status = "Save failed: " .. tostring(result)
Ops.say(S, "Save failed: " .. tostring(result))
return false
end
output = result
@@ -397,10 +428,10 @@ function App.save()
S.historySavedToken = S.historyToken or 0
S._quitArmed = false
Ops.disarm(S)
S.status = "Saved " .. S.path
Ops.say(S, "Saved " .. S.path, "ok")
return true
end
S.status = "Save failed: " .. tostring(err)
Ops.say(S, "Save failed: " .. tostring(err))
return false
end
@@ -413,7 +444,7 @@ function App.reload()
applyLoaded(S.path, "Reloaded")
return not S.loadError
end
S.status = "Reload failed: " .. tostring(err)
Ops.say(S, "Reload failed: " .. tostring(err))
return false
end
@@ -430,7 +461,7 @@ function App.close()
end
if S.dirty and not S._quitArmed then
S._quitArmed = true
S.status = "Unsaved changes, Save first or click Close again to discard"
Ops.say(S, "Unsaved changes, Save first or click Close again to discard", "warn")
return false
end
S._closeRequested = true
@@ -800,11 +831,6 @@ local function drawStatusBar(x, y, w, h)
.. ctrl
.. "+R reload . Esc clear selection . arrows pan map . wheel scrolls lists"
)
-- The status message is the load-bearing half of this bar (every Ops verb
-- narrates through it); the keyboard map is decoration. On a phone the
-- two used to overlap because the hint was drawn unconditionally and the
-- status ellipsized against a negative budget (#715), so now the hint only
-- draws when the status still keeps a readable share of the bar.
local hintW = Kit.textWidth("tiny", hint)
local avail = w - 2 * pad - hintW - 14 * s
if avail >= 120 * s then
@@ -814,7 +840,7 @@ local function drawStatusBar(x, y, w, h)
end
Kit.text(
"mono",
Kit.ellipsize("mono", S.status or "", avail),
Kit.ellipsize("mono", S.note or "", avail),
x + pad,
y + (h - Kit.textHeight("mono")) / 2,
PAL.detail
@@ -860,8 +886,13 @@ function App.draw()
elseif padOn then
mx, my = padX, padY
end
if mouseClicked and clickX ~= nil and Toast.hit(S, clickX, clickY) then
Toast.clear(S)
mouseClicked = false
end
Motion.update()
Kit.beginFrame(mx, my, mouseClicked, wheelY)
Kit.trackControls = S.toast ~= nil and Kit.focus ~= nil and not Kit.desktop
mouseClicked = false
clickX, clickY = nil, nil
wheelY = 0
@@ -931,6 +962,7 @@ function App.draw()
local contentY = oy + railH + titleH + tabH
local contentH = sh - railH - titleH - tabH - statusH
S.toastBottom, S.toastHeader = nil, nil
local px, py = ox + 10 * s, contentY + 8 * s
local pw, ph = sw - 20 * s, math.max(1, contentH - 16 * s)
local ok, err = xpcall(function()
@@ -978,6 +1010,28 @@ function App.draw()
MovePicker.draw(S, Kit, width, height)
ItemPicker.draw(S, Kit, width, height)
end
if S.toast then
local a = toastArea
a.x, a.w, a.s, a.font = ox, sw, s, Kit.fonts.small
a.top, a.bottom, a.maxY, a.centerY, a.width, a.maxLines, a.place = nil, nil, nil, nil, nil, nil, nil
local hdr, fr = S.toastHeader, Kit.focusRect
if hdr then
a.x, a.w, a.width, a.maxLines = hdr.x, hdr.w, hdr.w, 2
a.centerY = hdr.y + hdr.h / 2
elseif Kit.focus and not Kit.desktop then
if fr then
a.top, a.bottom, a.maxY = fr.y + fr.h, fr.y, oy + sh - statusH
toastSlots.s, toastSlots.field, toastSlots.contentY = s, fr, contentY
toastSlots.minY, toastSlots.maxY, toastSlots.screenBottom = oy, oy + sh - statusH, oy + sh
a.place = placeClearOfControls
else
a.top = contentY
end
else
a.bottom = math.min(S.toastBottom or math.huge, oy + sh - statusH)
end
Toast.draw(S, a)
end
Kit.endFrame()
PadInput.draw()
@@ -1075,7 +1129,7 @@ function App.keypressed(key)
and (love.keyboard.isDown("lgui", "rgui") or love.keyboard.isDown("lctrl", "rctrl"))
if key == "escape" and (S.itemMenu or S.chromeMenu) then
S.itemMenu, S.chromeMenu = nil, false
Ops.say(S, "Menu closed")
Ops.note(S, "Menu closed")
return
end
if key == "escape" and S.tab == "map" and S.mapFocused then
@@ -1087,7 +1141,7 @@ function App.keypressed(key)
if key == "escape" then
S.editingMon = nil
Ops.disarm(S)
Ops.say(S, "Selection cleared")
Ops.note(S, "Selection cleared")
elseif key == "z" and mod then
if love.keyboard.isDown("lshift", "rshift") then
require("History").redo(S)
@@ -1114,8 +1168,8 @@ function App.wheelmoved(x, y)
wheelY = wheelY + (y or 0)
return
end
-- Only the map viewport spends the wheel on zoom. Search results and
-- spawn cards keep the launcher's normal scrolling under the pointer.
-- Only the map viewport spends the wheel on zoom. Search results keep the
-- launcher's normal scrolling under the pointer.
local mx, my = love.mouse.getPosition()
if
S.tab == "map"
@@ -1138,7 +1192,7 @@ function App.quit()
-- simple: block quit once and set status; user saves or force-quits again
if not S._quitArmed then
S._quitArmed = true
S.status = "Unsaved changes, save or press quit again"
Ops.say(S, "Unsaved changes, save or press quit again", "warn")
return true
end
end
+5
View File
@@ -1,6 +1,7 @@
-- A bounded, session-only undo stack. Snapshots are made on user mutations,
-- never on draw. Deep copies preserve metadata the editor does not expose.
local Copy = require("src.mods.Merge").deepCopy
local Toast = require("src.ui.kit.Toast")
local H = { LIMIT = 8 }
function H.capture(S)
return { save = Copy(S.save), dirty = S.dirty, token = S.historyToken or 0 }
@@ -30,23 +31,27 @@ end
function H.undo(S)
if not S.undoStack or #S.undoStack == 0 then
S.status = "Nothing to undo"
Toast.show(S, S.status, "info")
return false
end
S.redoStack = S.redoStack or {}
S.redoStack[#S.redoStack + 1] = H.capture(S)
restore(S, table.remove(S.undoStack))
S.status = "Undid the last edit"
Toast.show(S, S.status, "ok")
return true
end
function H.redo(S)
if not S.redoStack or #S.redoStack == 0 then
S.status = "Nothing to redo"
Toast.show(S, S.status, "info")
return false
end
S.undoStack = S.undoStack or {}
S.undoStack[#S.undoStack + 1] = H.capture(S)
restore(S, table.remove(S.redoStack))
S.status = "Redid the last edit"
Toast.show(S, S.status, "ok")
return true
end
return H
+2 -2
View File
@@ -346,7 +346,7 @@ local function drawSection(S, Kit, x, y, w, h, report, issues)
Ops.maxMon(S, mon)
end,
"good",
"Level 100, max IVs or DVs, friendship and PP. Spare EVs go to the strongest stats. Fully heals."
"Level 100, max IVs or DVs, friendship and PP. Spare EVs go to the strongest stats. Fully heals. In Gen 2 a shiny stays shiny, an Unown keeps its letter, and gender can change."
)
button("Full heal", function()
Ops.healMon(S, mon)
@@ -400,7 +400,7 @@ local function drawSection(S, Kit, x, y, w, h, report, issues)
else
button("Max all DVs", function()
Ops.maxDvs(S, mon)
end, "good", "Sets DVs to 15. In Gen 2 this can change gender and shininess.")
end, "good", "Sets DVs to 15. In Gen 2 a shiny keeps its shiny DVs (Atk 15, others 10), an Unown keeps its letter, and gender can change.")
button("Max stat training", function()
Ops.maxStatExp(S, mon)
end, "good", "Fills stat experience for every stat.")
+36 -1
View File
@@ -21,6 +21,10 @@ Kit.mouseX, Kit.mouseY = 0, 0
Kit.mouseClicked = false -- left button pressed this frame
Kit.wheelY = 0 -- wheel notches queued since the last frame (#595)
Kit.focus = nil -- id of the text field receiving keystrokes
Kit.focusRect = nil
local focusRect = {}
Kit.trackControls = false
local tracked, trackedN = {}, 0
Kit.time = 0
Kit.fonts = {}
Kit.scale = 1
@@ -72,6 +76,7 @@ function Kit.beginFrame(mx, my, clicked, wheel)
Kit.mouseClicked = clicked
Kit._fieldHit = false
Kit._focusDrawn = false
trackedN = 0
Kit.wheelY = wheel or 0
-- Mouse taps dispatch on release, so dragging never activates a row.
-- The real touch stream supplies its own precise drag deltas. SDL
@@ -214,6 +219,7 @@ end
function Kit.blur()
Kit.focus = nil
Kit.focusRect = nil
syncSoftKeyboard(nil) -- the soft keyboard follows focus down too (#529)
end
@@ -245,9 +251,36 @@ end
-- a modal they cannot take the tap, and the modal legitimately covers them.
Kit.audit = nil
local function track(x, y, w, h)
local c = Kit._clipRect
if c then
local x2, y2 = math.min(x + w, c.x + c.w), math.min(y + h, c.y + c.h)
x, y = math.max(x, c.x), math.max(y, c.y)
w, h = x2 - x, y2 - y
if w <= 0 or h <= 0 then return end
end
trackedN = trackedN + 1
local r = tracked[trackedN]
if not r then
r = {}
tracked[trackedN] = r
end
r[1], r[2], r[3], r[4] = x, y, w, h
end
function Kit.trackedControls()
return tracked, trackedN
end
local function audit(class, x, y, w, h, label)
if Kit.blockClicks then
return
end
if class == "control" and Kit.trackControls then
track(x, y, w, h)
end
local a = Kit.audit
if not a or Kit.blockClicks then
if not a then
return
end
local c = Kit._clipRect
@@ -685,6 +718,8 @@ function Kit.textfield(id, x, y, w, h, value, placeholder, opts)
end
if focused then
Kit._focusDrawn = true
focusRect.x, focusRect.y, focusRect.w, focusRect.h = x, y, w, h
Kit.focusRect = focusRect
-- raise (or hand off) the soft keyboard while this field owns focus (#529)
syncSoftKeyboard(id, x, y, w, h)
for _, edit in ipairs(edits) do
+11 -2
View File
@@ -178,7 +178,7 @@ end
function A.fixMon(S, mon)
local Ops, L = require("Ops"), require("Legality")
if L.mon(S, mon).errors == 0 then
return Ops.say(S, "No property errors to fix")
return Ops.say(S, "No property errors to fix", "info")
end
local staged, err = A.repair(S, mon)
if not staged then
@@ -205,7 +205,7 @@ function A.fixAll(S)
msg = msg .. "; " .. left .. " need a manual choice"
end
if fixed == 0 then
return require("Ops").say(S, left > 0 and msg or "No property errors to fix")
return require("Ops").say(S, left > 0 and msg or "No property errors to fix", left > 0 and "error" or "info")
end
return require("Ops").mark(S, msg .. ". Origin warnings stay for review.")
end
@@ -218,6 +218,7 @@ function A.maxMon(S, mon)
if staged.egg or staged.isEgg then
return Ops.say(S, "Hatch the egg before maxing it out")
end
local keptShiny
M.setLevel(S.data, staged, 100, g)
M.setHappiness(S.data, staged, 255, g)
if g == 3 then
@@ -240,6 +241,11 @@ function A.maxMon(S, mon)
for _, k in ipairs(keys) do
M.setEv(S.data, staged, k, math.max(staged.evs[k] or 0, 252), g)
end
elseif g == 2 then
keptShiny = M.maxGen2Dvs(S.data, staged, g)
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
staged.statExp[k] = 65535
end
else
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
M.setDv(S.data, staged, k, 15, g)
@@ -261,6 +267,9 @@ function A.maxMon(S, mon)
end
end
replace(mon, staged)
if keptShiny then
return Ops.mark(S, "Maxed level, stats, friendship and PP; fully healed (kept shiny: Def/Spd/Spc DVs stay 10)")
end
return Ops.mark(S, "Maxed level, stats, friendship and PP; fully healed")
end
-- pokefirered/src/wild_encounter.c:49 sUnownLetterSlots, by chamber.
+94
View File
@@ -191,6 +191,58 @@ local function unownReq(mon)
return require("src.core.game3.pokemon").unownLetter(mon.personality)
end
-- src/pokemon.c:5751 DRAW_SPINDA_SPOTS
local function spindaPid(base, tsv, accept, shinyOnly)
local bit = require("bit")
base = (tonumber(base) or 0) % 4294967296
local ob = {}
for i = 0, 3 do ob[i] = bit.band(bit.rshift(base, 8 * i), 0xFF) end
local best, bestCost
local function try(p)
if not accept(p) then return end
local cost = 0
for i = 0, 3 do
local b = bit.band(bit.rshift(p, 8 * i), 0xFF)
if b ~= ob[i] then
cost = cost + 1000 + math.abs(b % 16 - ob[i] % 16)
+ math.abs(bit.rshift(b, 4) - bit.rshift(ob[i], 4))
end
end
if not bestCost or cost < bestCost then best, bestCost = p, cost end
end
if shinyOnly then
for low = 0, 65535 do
local h0 = bit.bxor(low, tsv)
for s = 0, 7 do try(bit.bxor(h0, s) * 65536 + low) end
end
return best
end
try(base)
if best then return best end
local function with(p, i, v)
return p + (v - bit.band(bit.rshift(p, 8 * i), 0xFF)) * 256 ^ i
end
for i = 0, 3 do
for v = 0, 255 do
if v ~= ob[i] then try(with(base, i, v)) end
end
end
if best then return best end
for i = 0, 2 do
for j = i + 1, 3 do
for v = 0, 255 do
if v ~= ob[i] then
local pi = with(base, i, v)
for w = 0, 255 do
if w ~= ob[j] then try(with(pi, j, w)) end
end
end
end
end
end
return best
end
function MonOps.generatePid(species, otId, otSecretId, reqs)
reqs = reqs or {}
local PokemonG3 = require("src.core.game3.pokemon")
@@ -213,6 +265,30 @@ function MonOps.generatePid(species, otId, otSecretId, reqs)
local targetGender = reqs.gender
local targetShiny = reqs.shiny
if spId == PokemonG3.SPECIES_SPINDA and reqs.basePid ~= nil then
local p = spindaPid(reqs.basePid, trainerXor, function(p)
if p == 0 then return false end
if targetNature ~= nil and p % 25 ~= targetNature then return false end
local b0 = p % 256
if targetAbility ~= nil and b0 % 2 ~= targetAbility then return false end
if targetGender ~= nil and targetGender ~= "" and targetGender ~= "U" then
local g
if ratio == PokemonG3.GENDER_MALE then g = "M"
elseif ratio == PokemonG3.GENDER_FEMALE then g = "F"
elseif ratio == PokemonG3.GENDER_GENDERLESS then g = "U"
elseif ratio > b0 then g = "F"
else g = "M" end
if g ~= targetGender then return false end
end
if targetShiny ~= nil then
local isShiny = bit.bxor(trainerXor, bit.bxor(math.floor(p / 65536), p % 65536)) < 8
if isShiny ~= (targetShiny and true or false) then return false end
end
return true
end, targetShiny == true)
if p then return p end
end
-- 1. Determine valid low-byte (b0) for gender & ability slot
local validB0 = {}
for b = 0, 255 do
@@ -672,6 +748,24 @@ function MonOps.unownDvs(dvs, letter, keepShiny)
return best
end
-- pokecrystal/engine/gfx/color.asm:8 CheckShininess, pokecrystal/engine/battle/hidden_power.asm:13
function MonOps.maxGen2Dvs(data, mon, gen)
if type(mon) ~= "table" then return end
local Mon = require("src.battle.gen2.Mon")
local Unown = require("src.core.gen2.Unown")
mon.dvs = mon.dvs or {}
local keepShiny = mon.shiny == true or Mon.vanillaShiny(mon.dvs)
local out = keepShiny and { attack = 15, defense = 10, speed = 10, special = 10 }
or { attack = 15, defense = 15, speed = 15, special = 15 }
if mon.species == Unown.SPECIES then
out = MonOps.unownDvs(out, Unown.monLetter(mon), keepShiny) or out
end
for k, v in pairs(out) do mon.dvs[k] = v end
deriveGen2(data, mon)
MonOps.recalc(data, mon, gen)
return keepShiny and mon.shiny == true
end
function MonOps.setUnownForm(data, mon, letter, gen)
if type(mon) ~= "table" then return nil end
if gen == 3 then
+126 -98
View File
@@ -18,6 +18,7 @@ local Bag = require("src.inventory.Bag")
local MonOps = require("MonOps")
local Charmap = require("src.save_convert.data.charmap")
local Gen = require("Gen")
local Toast = require("src.ui.kit.Toast")
local Ops = {}
local G3 = require("Game3Adapter")
@@ -46,6 +47,12 @@ local function clamp(n, lo, hi)
end
Ops.clamp = clamp
local function speciesName(S, species)
local def = species and S.data and S.data.pokemon and S.data.pokemon[species]
return (def and def.name) or tostring(species or "")
end
Ops.speciesName = speciesName
local function stampNewMon(S, mon)
if Gen.ofState(S) == 3 then
mon.ot = (S.save.player and S.save.player.name) or S.save.name or "RED"
@@ -83,6 +90,15 @@ local function partyMailEntry(S, slot)
end
local function syncPartyMailSpecies(S, mon)
if Gen.ofState(S) == 3 then
local idx = tonumber(mon and mon.mail)
local rec = idx and partySlot(S, mon) and type(S.save.mail) == "table" and S.save.mail[idx + 1]
if type(rec) == "table" and (tonumber(rec.itemId) or 0) ~= 0 then
-- pokeemerald/src/mail_data.c:72, pokefirered/src/mail_data.c:65
rec.species = require("src.core.game3.mail").speciesToMailSpecies(mon.speciesId or mon.species, mon.personality)
end
return
end
if Gen.ofState(S) ~= 2 then return end
local slot = partySlot(S, mon)
if not slot then return end
@@ -121,12 +137,13 @@ end
-- ------------------------------------------------------------ the funnel
-- Mark the save dirty and say what changed. Also disarms any pending
-- destructive confirmation: doing something else is an implicit "no".
function Ops.mark(S, msg)
function Ops.mark(S, msg, kind)
S.revision = (S.revision or 0) + 1
S.historyToken = S.revision
S.dirty = true
S.status = msg or S.status
S.armed = nil
if msg then Toast.show(S, msg, kind or "ok") end
-- A fresh edit invalidates any prior "leave anyway" arming: quitting,
-- closing or opening another file has to be confirmed again, so a stale
-- confirmation from an earlier round of edits cannot discard these.
@@ -137,8 +154,14 @@ end
-- Status-only: used by the branches that refuse (party full, box full, no
-- cell selected). A refusal must still speak, it must just not dirty.
function Ops.say(S, msg)
function Ops.say(S, msg, kind, opts)
S.status = msg
Toast.show(S, msg, kind or "error", opts)
return false
end
function Ops.note(S, msg)
S.note = msg or ""
return false
end
@@ -156,6 +179,7 @@ function Ops.arm(S, id, msg)
end
S.armed, S.armedAt = id, t
S.status = msg
Toast.show(S, msg, "warn", { hold = Ops.ARM_SECONDS })
return false
end
@@ -171,6 +195,7 @@ function Ops.armLabel(S, id, label)
end
function Ops.disarm(S)
if S.armed and S.toast and S.toast.kind == "warn" and S.toast.text == S.status then Toast.clear(S) end
S.armed, S.armedAt = nil, nil
end
@@ -180,7 +205,7 @@ function Ops.selectParty(S, index)
if not mon then return false end
S.selectedParty = index
S.editingMon = mon
S.status = ("Selected party slot %d (%s)"):format(index, mon.species)
Ops.note(S, ("Selected party slot %d (%s)"):format(index, speciesName(S, mon.species)))
return true
end
@@ -193,7 +218,7 @@ function Ops.partyAdd(S)
table.insert(S.save.party, mon)
S.selectedParty = #S.save.party
S.editingMon = mon
return Ops.mark(S, ("Added %s Lv5 to party slot %d"):format(species, #S.save.party))
return Ops.mark(S, ("Added %s Lv5 to party slot %d"):format(speciesName(S, species), #S.save.party))
end
function Ops.partyRemove(S)
@@ -201,7 +226,7 @@ function Ops.partyRemove(S)
local mon = S.save.party[index]
if not mon then return Ops.say(S, "No party slot selected") end
if not Ops.arm(S, "party-remove",
("Remove %s from slot %d? Click again to confirm"):format(mon.species, index)) then
("Remove %s from slot %d? Click again to confirm"):format(speciesName(S, mon.species), index)) then
return false
end
table.remove(S.save.party, index)
@@ -213,7 +238,7 @@ function Ops.partyRemove(S)
if S.editingMon == mon then S.editingMon = nil end
S.selectedParty = clamp(index, 1, math.max(#S.save.party, 1))
S.editingMon = S.save.party[S.selectedParty]
return Ops.mark(S, ("Removed %s from the party"):format(mon.species))
return Ops.mark(S, ("Removed %s from the party"):format(speciesName(S, mon.species)))
end
-- delta is -1 (up) or +1 (down); the selection follows the mon.
@@ -222,14 +247,14 @@ function Ops.partyMove(S, delta)
local j = i + delta
local party = S.save.party
if not (party[i] and party[j]) then
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", "info")
end
party[i], party[j] = party[j], party[i]
if Gen.ofState(S) == 2 then
require("src.core.gen2.Mail").swapSlots(S.save, i, j)
end
S.selectedParty = j
return Ops.mark(S, ("Moved %s to slot %d"):format(party[j].species, j))
return Ops.mark(S, ("Moved %s to slot %d"):format(speciesName(S, party[j].species), j))
end
-- ------------------------------------------------------- selected mon edits
@@ -239,10 +264,10 @@ function Ops.setLevel(S, mon, level)
if not mon then return false end
local want = clamp(math.floor(level), 1, 100)
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", "info")
end
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(speciesName(S, mon.species), mon.level))
end
-- A catalog id is only usable as a real mon when its record carries what the
@@ -293,7 +318,7 @@ end
function Ops.setSpecies(S, mon, id)
if not mon then return false end
if id == mon.species then
return Ops.say(S, ("Already a %s"):format(tostring(id)))
return Ops.say(S, ("Already a %s"):format(speciesName(S, id)), "info")
end
if not Ops.speciesUsable(S, id) then
return Ops.say(S, ("%s has no usable base stats, cannot assign it")
@@ -314,7 +339,7 @@ function Ops.setSpecies(S, mon, id)
return Ops.say(S, ("Could not set %s: %s"):format(tostring(id), tostring(err)))
end
syncPartyMailSpecies(S, mon)
return Ops.mark(S, ("Species set to %s"):format(id))
return Ops.mark(S, ("Species set to %s"):format(speciesName(S, id)))
end
-- Kept for the keyboard and test path; the inspector opens the searchable
@@ -493,14 +518,14 @@ function Ops.boxAddSpecies(S, id)
S.selectedBoxSlot = boxInsert(S, box, mon)
S.editingMon = mon
return Ops.mark(S, ("Added %s Lv5 to box %d slot %d")
:format(id, S.selectedBox, S.selectedBoxSlot))
:format(speciesName(S, id), S.selectedBox, S.selectedBoxSlot))
end
function Ops.setDv(S, mon, key, value)
if not mon then return false end
local want = clamp(math.floor(value), 0, 15)
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), "info")
end
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))
@@ -512,7 +537,7 @@ function Ops.setEv(S, mon, key, value)
local cur = tonumber(mon.evs[key]) or 0
local applied = MonOps.setEv(S.data, mon, key, value, Gen.ofState(S))
if applied == cur then
return Ops.say(S, ("%s EV is already %d"):format(key:upper(), cur))
return Ops.say(S, ("%s EV is already %d"):format(key:upper(), cur), "info")
end
return Ops.mark(S, ("%s EV set to %d"):format(key:upper(), applied or 0))
end
@@ -520,7 +545,7 @@ end
function Ops.clearEvs(S, mon)
if not mon then return false end
MonOps.clearEvs(S.data, mon, Gen.ofState(S))
return Ops.mark(S, ("Cleared EVs for %s"):format(mon.species or "Pokémon"))
return Ops.mark(S, ("Cleared EVs for %s"):format(speciesName(S, mon.species or "Pokémon")))
end
function Ops.setIv(S, mon, key, value)
@@ -529,7 +554,7 @@ function Ops.setIv(S, mon, key, value)
local cur = tonumber(mon.ivs[key]) or 0
local applied = MonOps.setIv(S.data, mon, key, value, Gen.ofState(S))
if applied == cur then
return Ops.say(S, ("%s IV is already %d"):format(key:upper(), cur))
return Ops.say(S, ("%s IV is already %d"):format(key:upper(), cur), "info")
end
return Ops.mark(S, ("%s IV set to %d"):format(key:upper(), applied or 0))
end
@@ -537,7 +562,7 @@ end
function Ops.maxIvs(S, mon)
if not mon then return false end
MonOps.maxIvs(S.data, mon, Gen.ofState(S))
return Ops.mark(S, ("Maxed IVs (31) for %s"):format(mon.species or "Pokémon"))
return Ops.mark(S, ("Maxed IVs (31) for %s"):format(speciesName(S, mon.species or "Pokémon")))
end
function Ops.setPpUps(S, mon, slot, value)
@@ -549,7 +574,7 @@ function Ops.setPpUps(S, mon, slot, value)
return Ops.say(S, "Eggs and this move cannot use PP Ups")
end
if want == cur then
return Ops.say(S, ("Slot %d PP Up is already %d"):format(slot, want))
return Ops.say(S, ("Slot %d PP Up is already %d"):format(slot, want), "info")
end
local applied, maxPp = MonOps.setPpUps(S.data, mon, slot, want, Gen.ofState(S))
return Ops.mark(S, ("Slot %d PP Up set to %d (Max PP %d)"):format(slot, applied, maxPp))
@@ -565,7 +590,7 @@ end
function Ops.maxAllPpUps(S, mon)
if not mon then return false end
MonOps.maxAllPpUps(S.data, mon, Gen.ofState(S))
return Ops.mark(S, ("Restored PP and maximized supported PP Ups for %s"):format(mon.species or "Pokémon"))
return Ops.mark(S, ("Restored PP and maximized supported PP Ups for %s"):format(speciesName(S, mon.species or "Pokémon")))
end
-- Kept for tests and any keyboard path; the inspector opens the searchable
@@ -673,7 +698,7 @@ function Ops.setMove(S, mon, slot, id)
local candCanon = canonicalMoveId(S.data, id)
if (curCanon and candCanon and curCanon == candCanon)
or (current and tostring(current):upper() == tostring(id):upper()) then
return Ops.say(S, ("Move %d is already %s"):format(slot, tostring(id)))
return Ops.say(S, ("Move %d is already %s"):format(slot, tostring(id)), "info")
end
if not Ops.moveUsable(S, id) then
return Ops.say(S, ("%s is not a usable move, cannot assign it")
@@ -711,7 +736,7 @@ end
function Ops.clearMove(S, mon, slot)
if not (mon and mon.moves and mon.moves[slot]) then
return Ops.say(S, ("Move slot %d is already empty"):format(slot))
return Ops.say(S, ("Move slot %d is already empty"):format(slot), "info")
end
local id = moveId(mon, slot)
if Gen.ofState(S) == 3 then MonOps.clearMove(mon, slot) else mon.moves[slot] = nil end
@@ -736,7 +761,7 @@ function Ops.setExperience(S, mon, value)
return require("src.pokemon.Growth").expForLevel(def and def.growthRate or 0, level, S.data.growth_rates)
end
if exp > threshold(100) then return Ops.say(S, "Experience exceeds this species' level 100 maximum") end
if exp == Gen.exp(mon) then return Ops.say(S, "Experience is unchanged") end
if exp == Gen.exp(mon) then return Ops.say(S, "Experience is unchanged", "info") end
local level = 1
while level < 100 and exp >= threshold(level + 1) do level = level + 1 end
MonOps.setLevel(S.data, mon, level, g)
@@ -753,7 +778,7 @@ function Ops.setCurrentHp(S, mon, value)
if not mon or not require("Legality").integer(hp, 0, max) then
return Ops.say(S, "HP must be a whole number from 0 to " .. max)
end
if hp == mon.hp then return Ops.say(S, "HP is unchanged") end
if hp == mon.hp then return Ops.say(S, "HP is unchanged", "info") end
mon.hp = hp
return Ops.mark(S, "Current HP updated")
end
@@ -764,7 +789,7 @@ function Ops.setMonStatus(S, mon, status)
if status ~= nil and (not allowed[status] or (Gen.ofState(S) == 1 and status == "TOX")) then
return Ops.say(S, "Choose a status supported by this generation")
end
if mon.status == status then return Ops.say(S, "Status is unchanged") end
if mon.status == status then return Ops.say(S, "Status is unchanged", "info") end
mon.status = status
mon.sleep = status == "SLP" and 1 or nil
mon.toxicCounter = nil
@@ -813,7 +838,7 @@ function Ops.setTrainerProperty(S, key, value)
elseif key=="money" then Gen.setMoney(S.save,n)
elseif key=="coins" then Gen.setCoins(S.save,n) end
end
return Ops.mark(S,"Trainer "..key.." updated")
return Ops.mark(S, "Trainer " .. (({ id = "ID", secretId = "secret ID" })[key] or key) .. " updated")
end
function Ops.setMonProperty(S, mon, key, value)
@@ -826,7 +851,7 @@ function Ops.setMonProperty(S, mon, key, value)
if d.text and (Ops.nicknameLength(parsed) > d.max or not Ops.nicknameUsable(S, parsed)) then
return Ops.say(S, "Trainer name must use game characters and fit in " .. d.max .. " characters")
end
if P.get(mon, d) == parsed then return Ops.say(S, d.label .. " is unchanged") end
if P.get(mon, d) == parsed then return Ops.say(S, d.label .. " is unchanged", "info") end
P.write(mon, d, parsed)
if key == "personality" then
mon.isShiny = nil
@@ -875,13 +900,13 @@ function Ops.resetMoves(S, mon)
MonOps.setMove(S.data, mon, slot, id)
end
return Ops.mark(S, ("Reset %s to its Lv%d learnset (%d moves)")
:format(mon.species, mon.level, #learned))
:format(speciesName(S, mon.species), mon.level, #learned))
end
function Ops.healMon(S, mon)
if not mon then return false end
if mon.hp == mon.stats.hp and not mon.status then
return Ops.say(S, ("%s is already at full HP"):format(mon.species))
return Ops.say(S, ("%s is already at full HP"):format(speciesName(S, mon.species)), "info")
end
mon.hp = mon.stats.hp
if mon.maxHp then mon.maxHp = mon.stats.hp end
@@ -897,7 +922,7 @@ function Ops.healMon(S, mon)
if def then mv.pp = def.pp + ((mv.ppUps or 0) * math.floor(def.pp / 5)) end
end
end
return Ops.mark(S, ("Healed %s to %d/%d HP"):format(mon.species, mon.hp, mon.stats.hp))
return Ops.mark(S, ("Healed %s to %d/%d HP"):format(speciesName(S, mon.species), mon.hp, mon.stats.hp))
end
-- ----------------------------------------------------------------- nicknames
@@ -979,12 +1004,6 @@ function Ops.nicknameUsable(S, name)
end)
end
-- The species' display name, what an un-nicknamed mon reads as.
local function speciesName(S, species)
local def = species and S.data.pokemon[species]
return (def and def.name) or tostring(species or "")
end
-- The input gate for the inspector's nickname field. Given the whole draft
-- (existing text plus this frame's keystrokes and any paste), return the
-- version the game can actually hold: every glyph kept draws in the game
@@ -1018,11 +1037,11 @@ function Ops.setNickname(S, mon, name)
return Ops.clearNickname(S, mon)
end
if name == mon.nickname then
return Ops.say(S, ("Already nicknamed %s"):format(name))
return Ops.say(S, ("Already nicknamed %s"):format(name), "info")
end
if name == speciesName(S, mon.species) then
if mon.nickname == nil then
return Ops.say(S, ("%s is already un-nicknamed"):format(mon.species))
return Ops.say(S, ("%s is already un-nicknamed"):format(speciesName(S, mon.species)), "info")
end
mon.nickname = nil
return Ops.mark(S, ("%s matches its standard name; nickname cleared")
@@ -1036,16 +1055,16 @@ function Ops.setNickname(S, mon, name)
"That name has characters the game cannot render or export cleanly")
end
mon.nickname = name
return Ops.mark(S, ("Nicknamed %s \"%s\""):format(mon.species, name))
return Ops.mark(S, ("Nicknamed %s \"%s\""):format(speciesName(S, mon.species), name))
end
function Ops.clearNickname(S, mon)
if not mon then return Ops.say(S, "Pick a slot first") end
if mon.nickname == nil then
return Ops.say(S, ("%s has no nickname to clear"):format(mon.species))
return Ops.say(S, ("%s has no nickname to clear"):format(speciesName(S, mon.species)))
end
mon.nickname = nil
return Ops.mark(S, ("Cleared %s's nickname"):format(mon.species))
return Ops.mark(S, ("Cleared %s's nickname"):format(speciesName(S, mon.species)))
end
-- ------------------------------------------------------------------ boxes
@@ -1085,7 +1104,7 @@ function Ops.selectBox(S, index)
S.save.currentBox = S.selectedBox
if Gen.ofState(S) == 3 then S.save.storage.currentBox = S.selectedBox end
local box = Ops.boxes(S)[S.selectedBox]
S.status = ("Box %d (%d/%d)"):format(S.selectedBox, Ops.boxSize(S, box), Ops.boxCapacity(S))
Ops.note(S, ("Box %d (%d/%d)"):format(S.selectedBox, Ops.boxSize(S, box), Ops.boxCapacity(S)))
return true
end
@@ -1099,10 +1118,10 @@ function Ops.selectBoxSlot(S, index)
S.selectedBoxSlot = clamp(index, 1, Ops.boxCapacity(S))
local mon = box[S.selectedBoxSlot]
S.editingMon = mon
S.status = mon
Ops.note(S, mon
and ("Selected %s Lv%d in box %d slot %d")
:format(mon.species, mon.level, S.selectedBox, S.selectedBoxSlot)
or ("Box %d slot %d is empty"):format(S.selectedBox, S.selectedBoxSlot)
:format(speciesName(S, mon.species), mon.level, S.selectedBox, S.selectedBoxSlot)
or ("Box %d slot %d is empty"):format(S.selectedBox, S.selectedBoxSlot))
return true
end
@@ -1120,7 +1139,7 @@ function Ops.boxAdd(S)
S.selectedBoxSlot = boxInsert(S, box, mon)
S.editingMon = mon
return Ops.mark(S, ("Added %s Lv5 to box %d slot %d")
:format(species, S.selectedBox, S.selectedBoxSlot))
:format(speciesName(S, species), S.selectedBox, S.selectedBoxSlot))
end
function Ops.withdraw(S)
@@ -1142,7 +1161,7 @@ function Ops.withdraw(S)
end
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#Ops.boxes(S)[S.selectedBox], 1))
S.selectedParty = #S.save.party
return Ops.mark(S, ("Withdrew %s to party slot %d"):format(mon.species, #S.save.party))
return Ops.mark(S, ("Withdrew %s to party slot %d"):format(speciesName(S, mon.species), #S.save.party))
end
function Ops.release(S)
@@ -1150,13 +1169,13 @@ function Ops.release(S)
local mon = box[S.selectedBoxSlot]
if not mon then return Ops.say(S, "No box slot selected") end
if not Ops.arm(S, "box-release",
("Release %s permanently? Click again to confirm"):format(mon.species)) then
("Release %s permanently? Click again to confirm"):format(speciesName(S, mon.species))) then
return false
end
boxRemove(S, box, S.selectedBoxSlot)
if S.editingMon == mon then S.editingMon = nil end
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#box, 1))
return Ops.mark(S, ("Released %s"):format(mon.species))
return Ops.mark(S, ("Released %s"):format(speciesName(S, mon.species)))
end
-- Follows BoxesMod.deposit: fills the current box first, then the next box
@@ -1174,7 +1193,7 @@ function Ops.deposit(S)
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))
return Ops.mark(S, ("Deposited %s into box %d"):format(speciesName(S, mon.species), boxIndex))
end
if Gen.ofState(S) == 3 then
local boxes = Ops.boxes(S)
@@ -1187,7 +1206,7 @@ function Ops.deposit(S)
S.selectedParty = clamp(i, 1, math.max(#S.save.party, 1))
S.selectedBox, S.selectedBoxSlot = b, slot
if S.editingMon == mon then S.editingMon = nil end
return Ops.mark(S, ("Deposited %s into box %d"):format(mon.species, b))
return Ops.mark(S, ("Deposited %s into box %d"):format(speciesName(S, mon.species), b))
end
end
return Ops.say(S, "Every box is full")
@@ -1200,7 +1219,7 @@ function Ops.deposit(S)
S.selectedParty = clamp(i, 1, math.max(#S.save.party, 1))
S.selectedBox = boxNum
if S.editingMon == mon then S.editingMon = nil end
return Ops.mark(S, ("Deposited %s into box %d"):format(mon.species, boxNum))
return Ops.mark(S, ("Deposited %s into box %d"):format(speciesName(S, mon.species), boxNum))
end
-- ------------------------------------------------------------------ items
@@ -1209,7 +1228,7 @@ function Ops.addMoney(S, delta)
local want = clamp(have + delta, 0, Ops.MONEY_MAX)
if want == have then
return Ops.say(S, delta < 0 and "Money is already $0"
or ("Money is already capped at $%d"):format(Ops.MONEY_MAX))
or ("Money is already capped at $%d"):format(Ops.MONEY_MAX), "info")
end
Gen.setMoney(S.save, want)
return Ops.mark(S, ("Money set to $%d"):format(want))
@@ -1228,7 +1247,7 @@ function Ops.addCoins(S, delta)
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))
or ("Coins are already capped at %d"):format(Ops.COIN_MAX), "info")
end
Gen.setCoins(S.save, want)
return Ops.mark(S, ("Coins set to %d"):format(want))
@@ -1268,12 +1287,14 @@ local function rowCount(S, pc, id, slot)
end
end
local function changeG3(S, pc, id, quantity)
local function changeG3(S, pc, id, quantity, msg)
if not id then return Ops.say(S, "Pick an item first") end
if not G3.change(S.data, S.save, pc, { { id = id, qty = quantity } }) then
return Ops.say(S, "Item change refused: quantity or storage capacity")
end
return Ops.mark(S, ("%s x%d%s"):format(tostring(id), quantity, pc and " in PC storage" or ""))
local where = pc and "PC storage" or "the bag"
if quantity <= 0 then return Ops.mark(S, ("Removed %s from %s"):format(tostring(id), where)) end
return Ops.mark(S, (msg or "%s x%d in %s"):format(tostring(id), quantity, where))
end
local function g3Max(S, id, pc)
@@ -1290,7 +1311,7 @@ local function maxG3(S, pc)
top = math.max(top, cap)
end
end
if #changes == 0 then return Ops.say(S, "Every stack is already maxed") end
if #changes == 0 then return Ops.say(S, "Every stack is already maxed", "info") end
if not G3.change(S.data, S.save, pc, changes) then return Ops.say(S, "Item changes refused") end
return Ops.mark(S, ("Maxed %d stacks to x%d"):format(#changes, top))
end
@@ -1298,7 +1319,7 @@ end
function Ops.addToBag(S, id)
if not id then return Ops.say(S, "Pick an item first") end
S.save.inventory = S.save.inventory or {}
if Gen.ofState(S) == 3 then return changeG3(S, false, id, G3.quantity(S.data, S.save, false, id) + 1) end
if Gen.ofState(S) == 3 then return changeG3(S, false, id, G3.quantity(S.data, S.save, false, id) + 1, "Added %s x%d to %s") end
local pocket = Bag.pocketOf(id, S.data)
local capacity = Bag.capacity(S.data, pocket)
if Bag.add(S.save, id, 1, S.data) then
@@ -1316,7 +1337,7 @@ function Ops.bagAdjust(S, id, delta, slot)
if Gen.ofState(S) == 3 then return changeG3(S, false, id, math.max(0, G3.quantity(S.data, S.save, false, id) + delta)) end
if delta > 0 then
if have >= Ops.stackMax(S) and not splits(S, id) then
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)), "info")
end
if not Bag.add(S.save, id, delta, S.data) then
local pocket = Bag.pocketOf(id, S.data)
@@ -1379,7 +1400,7 @@ function Ops.bagMax(S, id)
end
local target = stackTarget(S, id, have)
if have >= target then
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)), "info")
end
Bag.add(S.save, id, target - have, S.data)
return Ops.mark(S, ("%s x%d"):format(tostring(id), target))
@@ -1411,7 +1432,7 @@ function Ops.bagMaxAll(S)
end
end
if n == 0 then
return Ops.say(S, ("Every bag stack is already at x%d"):format(Ops.stackMax(S)))
return Ops.say(S, ("Every bag stack is already at x%d"):format(Ops.stackMax(S)), "info")
end
return Ops.mark(S, ("Maxed %d bag stack%s to x%d")
:format(n, n == 1 and "" or "s", Ops.stackMax(S)))
@@ -1456,7 +1477,7 @@ end
function Ops.bagSort(S, mode)
if not ITEM_SORT_KEYS[mode] then return false end
local order = Bag.order(S.save, S.data)
if #order < 2 then return Ops.say(S, "Nothing to sort in the bag") end
if #order < 2 then return Ops.say(S, "Nothing to sort in the bag", "info") end
local sorted = itemRows(S, order, mode)
for i = 1, #sorted do order[i] = sorted[i] end
S.bagOffset = 0
@@ -1532,7 +1553,7 @@ local function slotMax(S, id)
end
function Ops.addToPc(S, id)
if Gen.ofState(S) == 3 then return changeG3(S, true, id, G3.quantity(S.data, S.save, true, id) + 1) end
if Gen.ofState(S) == 3 then return changeG3(S, true, id, G3.quantity(S.data, S.save, true, id) + 1, "Added %s x%d to %s") end
if not id then return Ops.say(S, "Pick an item first") end
local pc = Ops.pcItems(S)
if not pc[id] and not pcStacksFree(S, id, 1) then
@@ -1540,7 +1561,7 @@ function Ops.addToPc(S, id)
end
local cur = itemQty(pc, id)
if cur >= slotMax(S, id) then
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), slotMax(S, id)))
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), slotMax(S, id)), "info")
end
pc[id] = math.min(slotMax(S, id), cur + 1)
return Ops.mark(S, ("%s x%d in PC storage"):format(tostring(id), pc[id]))
@@ -1561,7 +1582,7 @@ function Ops.pcAdjust(S, id, delta, slot)
end
local split = delta > 0 and splits(S, id)
if delta > 0 and cur >= slotMax(S, id) and not split then
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), slotMax(S, id)))
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), slotMax(S, id)), "info")
end
if split and not pcStacksFree(S, id, delta) then
return Ops.say(S, ("PC item storage is full (%d stacks)"):format(pcCapacity(S)))
@@ -1603,7 +1624,7 @@ function Ops.pcMax(S, id)
end
local target = stackTarget(S, id, cur)
if cur >= target then
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)), "info")
end
pc[id] = target
return Ops.mark(S, ("%s x%d in PC storage"):format(tostring(id), target))
@@ -1637,7 +1658,7 @@ function Ops.pcMaxAll(S)
end
end
if n == 0 then
return Ops.say(S, ("Every PC stack is already at x%d"):format(Ops.stackMax(S)))
return Ops.say(S, ("Every PC stack is already at x%d"):format(Ops.stackMax(S)), "info")
end
return Ops.mark(S, ("Maxed %d PC stack%s to x%d")
:format(n, n == 1 and "" or "s", Ops.stackMax(S)))
@@ -1780,7 +1801,7 @@ function Ops.clearTrainers(S)
for _, id in ipairs(ids) do
if Gen.getFlag(S.save, id) then count = count + 1 end
end
if count == 0 then return Ops.say(S, "Trainers are already empty") end
if count == 0 then return Ops.say(S, "Trainers are already empty", "info") end
if not Ops.arm(S, "clear-trainers",
("Clear all %d defeated trainers? Click again to confirm"):format(count)) then
return false
@@ -1808,7 +1829,7 @@ function Ops.clearItems(S)
for _, id in ipairs(ids) do
if Gen.getFlag(S.save, id) then count = count + 1 end
end
if count == 0 then return Ops.say(S, "Items taken are already empty") end
if count == 0 then return Ops.say(S, "Items taken are already empty", "info") end
if not Ops.arm(S, "clear-items",
("Clear all %d items taken? Click again to confirm"):format(count)) then
return false
@@ -1831,7 +1852,7 @@ function Ops.clearToggles(S)
for _, id in ipairs(ids) do
if Gen.getFlag(S.save, id) then count = count + 1 end
end
if count == 0 then return Ops.say(S, "Object toggles are already empty") end
if count == 0 then return Ops.say(S, "Object toggles are already empty", "info") end
if not Ops.arm(S, "clear-toggles",
("Clear all %d object toggles? Click again to confirm"):format(count)) then
return false
@@ -1844,7 +1865,7 @@ function Ops.clearToggles(S)
end
local count = 0
for _ in pairs(S.save.objectToggles or {}) do count = count + 1 end
if count == 0 then return Ops.say(S, "Object toggles are already empty") end
if count == 0 then return Ops.say(S, "Object toggles are already empty", "info") end
if not Ops.arm(S, "clear-toggles",
("Clear all %d map object toggles? Click again to confirm"):format(count)) then
return false
@@ -1856,7 +1877,7 @@ end
function Ops.clearTable(S, tableKey, label)
local count = 0
for _ in pairs(S.save[tableKey] or {}) do count = count + 1 end
if count == 0 then return Ops.say(S, ("%s is already empty"):format(label)) end
if count == 0 then return Ops.say(S, ("%s is already empty"):format(label), "info") end
if not Ops.arm(S, "clear-" .. tableKey,
("Clear all %d %s entries? Click again to confirm"):format(count, label)) then
return false
@@ -1976,7 +1997,7 @@ function Ops.dexStamp(S)
if type(m) == "table" then stamp(m) end
end
end
if n == 0 then return Ops.say(S, "Party and boxes are already all in the dex") end
if n == 0 then return Ops.say(S, "Party and boxes are already all in the dex", "info") end
return Ops.mark(S, ("Owned %d more species from party + boxes"):format(n))
end
@@ -2134,16 +2155,16 @@ function Ops.setLastOutdoor(S, map)
S.save.healMap = S.mapId
S.save.healX = cell.cx
S.save.healY = cell.cy
return Ops.mark(S, ("heal location set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
return Ops.mark(S, ("Heal location set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
elseif Gen.ofState(S) == 2 then
S.save.spawn = S.mapId
return Ops.mark(S, ("spawn set to %s"):format(S.mapId))
return Ops.mark(S, ("Spawn point set to %s"):format(S.mapId))
end
if not Ops.isOutdoor(S, map) then
return Ops.say(S, S.mapId .. " doesn't look outdoor (no connections, not visited)")
end
S.save.lastOutdoor = { id = S.mapId, x = cell.cx, y = cell.cy }
return Ops.mark(S, ("lastOutdoor set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
return Ops.mark(S, ("Last outdoor map set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
end
function Ops.setLastHeal(S)
@@ -2153,13 +2174,13 @@ function Ops.setLastHeal(S)
S.save.healMap = S.mapId
S.save.healX = cell.cx
S.save.healY = cell.cy
return Ops.mark(S, ("heal location set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
return Ops.mark(S, ("Heal location set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
elseif Gen.ofState(S) == 2 then
S.save.spawn = S.mapId
return Ops.mark(S, ("spawn set to %s"):format(S.mapId))
return Ops.mark(S, ("Spawn point set to %s"):format(S.mapId))
end
S.save.lastHeal = { map = S.mapId, x = cell.cx, y = cell.cy }
return Ops.mark(S, ("lastHeal set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
return Ops.mark(S, ("Last heal set to %s (%d,%d)"):format(S.mapId, cell.cx, cell.cy))
end
function Ops.itemHoldable(S, id)
@@ -2190,17 +2211,17 @@ function Ops.setHeldItem(S, mon, id)
local was = mon.item or mon.heldItem
MonOps.setHeldItem(S.data, mon, def.itemId or def.id or id, Gen.ofState(S))
syncPartyMailHeldItem(S, mon, was, id)
return Ops.mark(S, ("%s now holds %s"):format(mon.species, id))
return Ops.mark(S, ("%s now holds %s"):format(speciesName(S, mon.species), itemName(S, 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 mon.friendship or 0) then
return Ops.say(S, ("Happiness is already %d"):format(want))
return Ops.say(S, ("Happiness is already %d"):format(want), "info")
end
MonOps.setHappiness(S.data, mon, want, Gen.ofState(S))
return Ops.mark(S, ("%s happiness %d"):format(mon.species, want))
return Ops.mark(S, ("%s happiness %d"):format(speciesName(S, mon.species), want))
end
function Ops.setNature(S, mon, natureId)
@@ -2209,7 +2230,7 @@ function Ops.setNature(S, mon, natureId)
MonOps.setNature(S.data, mon, natureId, Gen.ofState(S))
local SummaryData = require("src.core.game3.summary_data")
local name = SummaryData.NATURES[natureId]
return Ops.mark(S, ("%s nature set to %s"):format(mon.species, name))
return Ops.mark(S, ("%s nature set to %s"):format(speciesName(S, mon.species), name))
end
function Ops.setAbility(S, mon, abilitySlot)
@@ -2220,7 +2241,7 @@ function Ops.setAbility(S, mon, abilitySlot)
if abilitySlot==1 and (not pair[2] or pair[2]==0) then return Ops.say(S,"This species has only one ability") end
end
MonOps.setAbility(S.data, mon, abilitySlot, Gen.ofState(S))
return Ops.mark(S, ("%s ability set to slot %d"):format(mon.species, abilitySlot + 1))
return Ops.mark(S, ("%s ability set to slot %d"):format(speciesName(S, mon.species), abilitySlot + 1))
end
function Ops.setMonGender(S, mon, gender)
@@ -2232,13 +2253,13 @@ function Ops.setMonGender(S, mon, gender)
if ratio==255 or (ratio==0 and gender~="M") or (ratio==254 and gender~="F") then return Ops.say(S,"This species has a fixed gender") end
end
MonOps.setGender(S.data, mon, gender, Gen.ofState(S))
return Ops.mark(S, ("%s gender set to %s"):format(mon.species, tostring(gender)))
return Ops.mark(S, ("%s gender set to %s"):format(speciesName(S, mon.species), tostring(gender)))
end
function Ops.setShiny(S, mon, shiny)
if not mon then return false end
MonOps.setShiny(S.data, mon, shiny, Gen.ofState(S))
return Ops.mark(S, ("%s %s"):format(mon.species, shiny and "is now shiny!" or "is no longer shiny"))
return Ops.mark(S, ("%s %s"):format(speciesName(S, mon.species), shiny and "is now shiny!" or "is no longer shiny"))
end
Ops.UNOWN_FORM_NAMES_G3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?"
@@ -2279,10 +2300,11 @@ function Ops.setUnownForm(S, mon, letter)
letter = math.floor(tonumber(letter) or -1)
local name = Ops.unownFormName(S, letter)
if not name or name == "" then return Ops.say(S, "Unknown Unown form") end
if letter == current then return Ops.say(S, ("UNOWN is already %s"):format(name)) end
if letter == current then return Ops.say(S, ("UNOWN is already %s"):format(name), "info") end
local g = Gen.ofState(S)
local ok, lostShiny = MonOps.setUnownForm(S.data, mon, letter, g)
if not ok then return Ops.say(S, ("UNOWN %s cannot keep this nature and shininess"):format(name)) end
syncPartyMailSpecies(S, mon)
if lostShiny then
return Ops.mark(S, ("UNOWN is now %s (no longer shiny: only I and V can be shiny)"):format(name))
end
@@ -2293,10 +2315,10 @@ 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))
return Ops.say(S, ("Pokerus is already %d"):format(want), "info")
end
mon.pokerus = want
return Ops.mark(S, ("%s pokerus byte %d"):format(mon.species, want))
return Ops.mark(S, ("%s Pokérus byte %d"):format(speciesName(S, mon.species), want))
end
-- engine/pokemon/caught_data.asm:169-172
@@ -2314,11 +2336,11 @@ function Ops.setCaughtTime(S, mon, value)
if not caughtGuard(S, mon) then return false end
local want = clamp(math.floor(tonumber(value) or 0), 0, 3)
if want == (mon.caughtTime or 0) then
return Ops.say(S, ("Caught time is already %s"):format(Ops.CAUGHT_TIMES[want + 1]))
return Ops.say(S, ("Caught time is already %s"):format(Ops.CAUGHT_TIMES[want + 1]), "info")
end
mon.caughtTime = want
return Ops.mark(S, ("%s caught time %s")
:format(mon.species, Ops.CAUGHT_TIMES[want + 1]))
:format(speciesName(S, mon.species), Ops.CAUGHT_TIMES[want + 1]))
end
-- constants/pokemon_data_constants.asm:120-121
@@ -2327,10 +2349,10 @@ function Ops.setCaughtLevel(S, mon, value)
local Mon = require("src.battle.gen2.Mon")
local want = clamp(math.floor(tonumber(value) or 0), 0, Mon.CAUGHT_LEVEL_MASK)
if want == (mon.caughtLevel or 0) then
return Ops.say(S, ("Caught level is already %d"):format(want))
return Ops.say(S, ("Caught level is already %d"):format(want), "info")
end
mon.caughtLevel = want
return Ops.mark(S, ("%s caught level %d"):format(mon.species, want))
return Ops.mark(S, ("%s caught level %d"):format(speciesName(S, mon.species), want))
end
-- constants/landmark_constants.asm:111-113
@@ -2341,11 +2363,11 @@ function Ops.setCaughtLocation(S, mon, value)
local want = math.floor(tonumber(value) or 0) % span
if want == (mon.caughtLocation or 0) then
return Ops.say(S, ("Caught location is already %s")
:format(Gen.landmarkName(S.data, want)))
:format(Gen.landmarkName(S.data, want)), "info")
end
mon.caughtLocation = want
return Ops.mark(S, ("%s caught at %s")
:format(mon.species, Gen.landmarkName(S.data, want)))
:format(speciesName(S, mon.species), Gen.landmarkName(S.data, want)))
end
function Ops.setCaughtByGender(S, mon, gender)
@@ -2353,10 +2375,10 @@ function Ops.setCaughtByGender(S, mon, gender)
local Mon = require("src.battle.gen2.Mon")
local want = Mon.caughtGenderOf(gender)
if want == mon.caughtByGender then
return Ops.say(S, ("Caught by is already %s"):format(tostring(want or "none")))
return Ops.say(S, ("Caught by is already %s"):format(tostring(want or "none")), "info")
end
mon.caughtByGender = want
return Ops.mark(S, ("%s caught by %s"):format(mon.species, tostring(want or "none")))
return Ops.mark(S, ("%s caught by %s"):format(speciesName(S, mon.species), tostring(want or "none")))
end
function Ops.setPlayerGender(S, gender)
@@ -2364,7 +2386,7 @@ function Ops.setPlayerGender(S, gender)
return Ops.say(S, "This game has no player gender")
end
if Gen.playerGender(S.save) == gender then
return Ops.say(S, ("Player is already %s"):format(tostring(gender)))
return Ops.say(S, ("Player is already %s"):format(tostring(gender)), "info")
end
return Ops.mark(S, ("Player gender %s"):format(Gen.setPlayerGender(S.save, gender)))
end
@@ -2418,6 +2440,12 @@ function Ops.maxMon(S, mon)
return require("MonActions").maxMon(S, mon)
end
function Ops.maxDvs(S, mon)
if Gen.ofState(S) == 2 then
if MonOps.maxGen2Dvs(S.data, mon, 2) then
return Ops.mark(S, "Maxed DVs (kept shiny: Def/Spd/Spc stay 10)")
end
return Ops.mark(S, "Maxed DVs")
end
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
MonOps.setDv(S.data, mon, k, 15, Gen.ofState(S))
end
+9
View File
@@ -78,4 +78,13 @@ function PickerChrome.closeSize(Kit)
return math.max(PickerChrome.tapMin(Kit), math.floor(30 * ((Kit and Kit.scale) or 1)))
end
local header = {}
function PickerChrome.toastHeader(S, Kit, cx, cy, inner, headH)
local s = (Kit and Kit.scale) or 1
header.x, header.y, header.h = cx, cy, headH
header.w = math.max(1, inner - PickerChrome.closeSize(Kit) - 10 * s)
S.toastHeader = header
return header
end
return PickerChrome
+4 -2
View File
@@ -43,8 +43,10 @@ the scrim; choosing a destination closes the popup and slides to that section.
Popups support finger/wheel scrolling, keyboard selection, Escape, a close icon
and outside-tap dismissal. They shield the page underneath from clicks and scrolling.
Short screens use compact storage
actions and an item Tools menu. Map browsing splits into Maps, View and Spawn
sections on phones; storage and inventory actions stay in their viewport.
actions and an item Tools menu. Map browsing splits into Maps and View
sections on phones; Player, Heal (Spawn in Gen 2) and Outdoor (Gen 1) buttons
sit on top of the map in every layout and act on the selected cell; storage and
inventory actions stay in their viewport.
Safe-area margins protect controls around notches.
Short landscape map views have an expand icon that gives the map the editor's
full content area; the back icon restores the editor chrome.
+1
View File
@@ -36,6 +36,7 @@ function State.new()
-- chrome
tab = "party", -- party|boxes|items|events|map|dex
status = "",
note = "",
armed = nil, -- id of the destructive button awaiting confirmation
armedAt = nil, -- when it was armed (Ops.ARM_SECONDS to commit)
+2 -2
View File
@@ -310,7 +310,7 @@ local function drawSection(S, Kit, x, y, w, h)
function(value)
S.eventsOffset = 0
Ops.disarm(S)
Ops.say(S, hints[value] or "")
Ops.note(S, hints[value] or "")
end
)
local px = cx + chooserW + 10 * s
@@ -340,7 +340,7 @@ local function drawSection(S, Kit, x, y, w, h)
then
S.eventFilter = ""
Kit.blur()
Ops.say(S, "Filter cleared")
Ops.note(S, "Filter cleared")
end
local hintY = filterY + pillH + 10 * s
+5 -40
View File
@@ -19,40 +19,6 @@ local PAL = Theme.PAL
local Picker = {}
local FIELD_ID = "item-picker"
local FEEDBACK_SECONDS = 2.5
local function feedback(S, Kit, changed, dest)
local p = S.itemPicker
if p then
p.feedback = {
text = changed and (dest == "pc" and "Added to PC" or "Added to Bag") or "Item not added",
good = changed,
at = love.timer and love.timer.getTime and love.timer.getTime() or Kit.time,
}
end
return changed
end
local function drawFeedback(p, Kit, x, y, w, h)
local toast = p.feedback
if not toast then return false end
local elapsed = math.max(0, Kit.time - toast.at)
if elapsed >= FEEDBACK_SECONDS then
p.feedback = nil
return false
end
local alpha = math.min(1, (FEEDBACK_SECONDS - elapsed) / 0.4)
local s, pad = Kit.scale, 8 * Kit.scale
local icon = 18 * s
local tw = math.min(w, Kit.textWidth("small", toast.text) + icon + 3 * pad)
local color = toast.good and PAL.green or PAL.red
Theme.fillRounded(x, y, tw, h, PAL.cardBody, 0.98 * alpha)
Theme.stroke(x, y, tw, h, Theme.radius(), color, 0.8 * alpha, 1)
Kit.icon(toast.good and "check" or "triangle-alert", x + pad, y + (h - icon) / 2, icon, color, alpha)
Kit.text("small", toast.text, x + icon + 2 * pad, y + (h - Kit.textHeight("small")) / 2, PAL.text, alpha)
return true
end
function Picker.results(S)
local p = S.itemPicker
local hits = Ops.itemSearch(S, p and p.query or "")
@@ -73,7 +39,7 @@ end
function Picker.commitFirst(S, Kit)
local hits = Picker.results(S)
if not hits[1] then
return Ops.say(S, "No item matches that")
return Ops.say(S, "No item matches that", "info")
end
return Picker.commit(S, Kit, hits[1])
end
@@ -93,9 +59,9 @@ function Picker.commit(S, Kit, id)
return changed
end
if dest == "pc" then
return feedback(S, Kit, Ops.addToPc(S, id), dest)
return Ops.addToPc(S, id)
end
return feedback(S, Kit, Ops.addToBag(S, id), dest)
return Ops.addToBag(S, id)
end
function Picker.draw(S, Kit, width, height)
@@ -134,9 +100,8 @@ function Picker.draw(S, Kit, width, height)
local closeW = PickerChrome.closeSize(Kit)
local captionH = Kit.textHeight("caption")
local headH = math.max(captionH, closeW)
if not drawFeedback(p, Kit, cx, cy, inner - closeW - 10 * s, headH) then
Kit.caption(cx, cy + (headH - captionH) / 2, "ADD AN ITEM")
end
PickerChrome.toastHeader(S, Kit, cx, cy, inner, headH)
Kit.caption(cx, cy + (headH - captionH) / 2, "ADD AN ITEM")
if
Kit.iconButton(
x + w - pad - closeW,
+146 -162
View File
@@ -3,8 +3,7 @@
-- Reuses the game's own MapLoader/TileRenderer/Warp so the editor's view
-- matches what the player would actually see.
--
-- Three columns: a searchable map list, the viewport, and the spawn
-- inspector. Overlays are drawn in this order so the selection always wins:
-- Overlays are drawn in this order so the selection always wins:
-- cyan hollow warp cell (clicking follows the warp)
-- red filled the save's player position
-- green / amber lastHeal / lastOutdoor
@@ -181,9 +180,9 @@ local function goToWarp(S, warp)
local dest = def.destMap or def.map
if dest then
MapBrowser.select(S, dest)
S.status = "Followed warp to " .. tostring(dest)
Ops.note(S, "Followed warp to " .. tostring(dest))
else
S.status = "Warp has no destination map"
Ops.say(S, "Warp has no destination map")
end
return
end
@@ -197,12 +196,12 @@ local function goToWarp(S, warp)
S.save.lastOutdoor = { id = S.mapId, x = def.x, y = def.y }
end
if def.destMap == "LAST_MAP" and not S.save.lastOutdoor then
S.status = "Can't follow warp: no remembered outdoor map (lastOutdoor unset)"
Ops.say(S, "Can't follow warp: no remembered outdoor map")
return
end
local ok, destMap, dx, dy = pcall(Warp.destination, S.data, def, S.save.lastOutdoor)
if not ok then
S.status = "Warp failed: " .. tostring(destMap)
Ops.say(S, "Warp failed: " .. tostring(destMap))
return
end
S.mapId = destMap
@@ -212,7 +211,7 @@ local function goToWarp(S, warp)
-- claim the lazy first-draw centering below, so it does not immediately
-- re-centre the destination map and lose the warp's landing cell
S._mapCenteredFor = destMap
S.status = "Followed warp to " .. destMap
Ops.note(S, "Followed warp to " .. destMap)
end
-- Screen-space point inside the viewport -> map cell, or nil if the point is
@@ -264,7 +263,7 @@ function MapBrowser.select(S, id)
S.mapClickCell = nil
S._mapCenteredFor = nil
MapBrowser.clearTouches(S)
S.status = "Viewing " .. id
Ops.note(S, "Viewing " .. id)
end
-- Called inside the viewport's translate+scale transform, so every rect is
@@ -412,6 +411,118 @@ function MapBrowser.preview(S)
return true, map, reason and ("Preview unavailable: " .. reason .. ". Coordinate grid remains available.")
end
local function insideOverlay(S, x, y)
for _, r in ipairs(S._mapOverlayRects or {}) do
if x >= r.x and x <= r.x + r.w and y >= r.y and y <= r.y + r.h then return true end
end
return false
end
MapBrowser.insideOverlay = insideOverlay
function MapBrowser.pointActions(S, map)
local pmap, px, py = playerPos(S)
local function at(id, cx, cy)
return id and ("%s (%d,%d)"):format(id, cx or 0, cy or 0) or "unset"
end
local heal = { id = "heal", label = "Heal", what = "Last heal", icon = "heart", run = Ops.setLastHeal }
local actions = {
{ id = "player", label = "Player", what = "Player", icon = "user-round", value = at(pmap, px, py),
run = Ops.setPlayerHere },
heal,
}
local generation = Gen.ofState(S)
if generation == 3 then
heal.value = at(S.save.healMap, S.save.healX, S.save.healY)
elseif generation == 2 then
heal.id, heal.label, heal.what = "spawn", "Spawn", "Spawn"
heal.value = tostring(S.save.spawn or "SPAWN_HOME")
else
local last, out = S.save.lastHeal, S.save.lastOutdoor
heal.value = last and at(last.map, last.x, last.y) or "unset"
actions[3] = {
id = "outdoor", label = "Outdoor", what = "Last outdoor", icon = "flag",
value = out and at(out.id, out.x, out.y) or "unset",
run = function(state) return Ops.setLastOutdoor(state, map) end,
refused = not Ops.isOutdoor(S, map),
}
end
for _, action in ipairs(actions) do
action.enabled = S.mapClickCell ~= nil and not action.refused
if not S.mapClickCell then
action.reason = ("%s: %s. Tap a cell first"):format(action.what, action.value)
end
end
return actions
end
local function layoutActions(S, Kit, actions, vx0, vy0, vinner, vh0)
local s = Kit.scale
local h, gapB, inset = Kit.controlH(), 6 * s, 6 * s
local right, top = vx0 + vinner - inset, vy0 + inset
local availW, availH = vinner - 2 * inset, vh0 - 2 * inset
local function place(compact, column)
local total, widest = -gapB, 0
for _, action in ipairs(actions) do
action.w = compact and h or Kit.buttonWidth(action.label, { font = "small", icon = action.icon }, h)
total = total + action.w + gapB
widest = math.max(widest, action.w)
end
if column then
if widest > availW or #actions * (h + gapB) - gapB > availH then return false end
local y = top
for _, action in ipairs(actions) do
action.rect = { x = right - action.w, y = y, w = action.w, h = h }
y = y + h + gapB
end
else
if total > availW or h > availH then return false end
local x = right - total
for _, action in ipairs(actions) do
action.rect = { x = x, y = top, w = action.w, h = h }
x = x + action.w + gapB
end
end
S._mapActionsCompact = compact
return true
end
if not (place(false, false) or place(false, true) or place(true, false) or place(true, true)) then
S._mapOverlayRects, S._mapActionsCompact = {}, nil
return 0
end
local rects, bottom = {}, top
for _, action in ipairs(actions) do
rects[#rects + 1] = action.rect
bottom = math.max(bottom, action.rect.y + h)
end
S._mapOverlayRects = rects
return bottom - vy0
end
local function drawActions(S, Kit, actions)
for _, action in ipairs(S._mapOverlayRects and #S._mapOverlayRects > 0 and actions or {}) do
local r = action.rect
local pressed
if S._mapActionsCompact then
pressed = Kit.iconButton(r.x, r.y, r.w, r.h, action.icon, action.label, {
kind = "accent", enabled = action.enabled,
})
else
pressed = Kit.button(r.x, r.y, r.w, r.h, action.label, {
kind = "accent", font = "small", icon = action.icon, enabled = action.enabled,
})
end
if pressed then
action.run(S)
elseif not action.enabled and Kit.press(r.x, r.y, r.w, r.h) then
if action.reason then
Ops.say(S, action.reason, "info")
else
action.run(S)
end
end
end
end
local function drawSection(S, Kit, x, y, w, h)
local s = Kit.scale
local gap = 20 * s
@@ -419,25 +530,24 @@ local function drawSection(S, Kit, x, y, w, h)
S.mapQuery = S.mapQuery or ""
S.mapZoom = clampZoom(S.mapZoom or 2)
-- Column plan (#715). Side by side, the list and spawn cards claim ~470
-- logical px before the viewport gets anything, and a portrait phone does
-- not have it: the old layout answered by laying the viewport out at a
-- Column plan (#715). A portrait phone cannot fit the list beside the
-- viewport: the old layout answered by laying the viewport out at a
-- negative width, which the scissor below rejected ("Can't set scissor
-- with negative width and/or height") and took the whole editor down.
-- Phones open Maps / View / Spawn as sliding pages. Every viewport
-- Phones open Maps / View as sliding pages. Every viewport
-- dimension is clamped so no window shape creates a negative scissor.
local listW = math.max(200 * s, math.min(260 * s, w * 0.2))
local sideW = math.max(230 * s, math.min(300 * s, w * 0.22))
local viewW = w - listW - sideW - 2 * gap
local stacked = S.mapFocused or h > w or viewW < 260 * s
local viewW = w - listW - gap
local stacked = S.mapFocused or h > w or viewW - sideW - gap < 260 * s
S._mapStacked = stacked
local lr, vr, sr -- list / viewport / spawn card rects
local lr, vr -- list / viewport rects
local row, gapNav = Kit.controlH(), 6 * s
local labelW = 48 * s
local inlineZoom = stacked and w > h and w >= 5 * row + labelW + 6 * gapNav + 100 * s
if stacked then
S.mapSection = S.mapSection or "view"
local sections = { { "maps", "Maps" }, { "view", "View" }, { "spawn", "Spawn" } }
if S.mapSection ~= "maps" then S.mapSection = "view" end
local sections = { { "maps", "Maps" }, { "view", "View" } }
if Kit.iconButton(x, y, row, row, S.mapFocused and "chevron-left" or "expand",
S.mapFocused and "Back to editor" or "Focus map") then
S.mapFocused = not S.mapFocused
@@ -484,11 +594,9 @@ local function drawSection(S, Kit, x, y, w, h)
y, h = y + row + gapNav, math.max(0, h - row - gapNav)
lr = { x = x, y = y, w = w, h = h }
vr = lr
sr = lr
else
lr = { x = x, y = y, w = listW, h = h }
vr = { x = x + listW + gap, y = y, w = math.max(0, viewW), h = h }
sr = { x = x + w - sideW, y = y, w = sideW, h = h }
end
-- --------------------------------------------------------- the map list
@@ -521,6 +629,9 @@ local function drawSection(S, Kit, x, y, w, h)
local gotoY = lr.y + lr.h - pad - gotoH
local pagerH = Kit.controlH()
local pagerY = gotoY - 10 * s - pagerH
if stacked and not compactList then
S.toastBottom = math.min(S.toastBottom or pagerY, pagerY)
end
local listTop = qy + Kit.controlH() + 10 * s
local mRowH = Kit.controlH()
local mGap = 4 * s
@@ -572,7 +683,7 @@ local function drawSection(S, Kit, x, y, w, h)
local pmap, px, py = playerPos(S)
if pmap then
MapBrowser.select(S, pmap)
Ops.say(S, ("Jumped to %s (%d,%d)"):format(pmap, px, py))
Ops.say(S, ("Jumped to %s (%d,%d)"):format(pmap, px, py), "ok")
else
Ops.say(S, "No player location on this save")
end
@@ -663,7 +774,7 @@ local function drawSection(S, Kit, x, y, w, h)
)
then
showPlayer(S)
Ops.say(S, "Centred on the player")
Ops.note(S, "Centred on the player")
end
end
local zoomY = vr.y + vpad
@@ -728,8 +839,11 @@ local function drawSection(S, Kit, x, y, w, h)
S._mapCenterPlayer, S.mapAutoFit = nil, false
end
local actions = MapBrowser.pointActions(S, map)
local barH = layoutActions(S, Kit, actions, vx0, vy0, vinner, vh0)
if Kit.mouseDown and not Kit.blockClicks and not S._mapPinch
and (S._mapDrag or Kit.hit(vx0, vy0, vinner, vh0)) then
and (S._mapDrag or (Kit.hit(vx0, vy0, vinner, vh0) and not insideOverlay(S, Kit.mouseX, Kit.mouseY))) then
local d = S._mapDrag
if not d then
S._mapDrag = { mx = Kit.mouseX, my = Kit.mouseY, camX = S.mapCamX, camY = S.mapCamY }
@@ -781,13 +895,15 @@ local function drawSection(S, Kit, x, y, w, h)
drawOverlays(S, map)
love.graphics.pop()
if previewReason then
Kit.textWrapped("tiny", previewReason, vx0 + 8 * s, vy0 + 8 * s, math.max(0, vinner - 16 * s), PAL.yellow)
Kit.textWrapped("tiny", previewReason, vx0 + 8 * s, vy0 + 8 * s + barH, math.max(0, vinner - 16 * s), PAL.yellow)
end
Kit.popClip()
end
drawActions(S, Kit, actions)
-- click handling: warp cells jump the view, everything else selects
if Kit.mouseClicked and not Kit.blockClicks and not S._mapPinch then
if Kit.mouseClicked and not Kit.blockClicks and not S._mapPinch
and not insideOverlay(S, Kit.mouseX, Kit.mouseY) then
local cx, cy = cellAtScreen(S, map, Kit, vx0, vy0, vinner, vh0)
if cx then
local warp = map:warpAtCell(cx, cy)
@@ -795,7 +911,7 @@ local function drawSection(S, Kit, x, y, w, h)
goToWarp(S, warp)
else
S.mapClickCell = { cx = cx, cy = cy }
S.status = string.format("Selected cell (%d,%d) on %s", cx, cy, S.mapId)
Ops.note(S, string.format("Selected cell (%d,%d) on %s", cx, cy, S.mapId))
end
end
end
@@ -806,12 +922,13 @@ local function drawSection(S, Kit, x, y, w, h)
local legend = {
{ PAL.blue, "warp", false },
{ PAL.red, "player", true },
{ PAL.green, "lastHeal", false },
{ PAL.yellow, "lastOutdoor", false },
{ PAL.green, "last heal", false },
{ PAL.yellow, "last outdoor", false },
}
if stacked then
if not inlineZoom then
local toolY = ly - row - 6 * s
S.toastBottom = math.min(S.toastBottom or toolY, toolY)
local fitW = Kit.buttonWidth("Fit", { font = "small" }, row)
local percentW = Kit.textWidth("tiny", "400%") + 4 * s
local toolsW = 3 * row + fitW + percentW + 4 * gapNav
@@ -828,7 +945,7 @@ local function drawSection(S, Kit, x, y, w, h)
Kit.text(
"tiny",
S.mapClickCell
and ("Selected (%d,%d) · set in Spawn"):format(S.mapClickCell.cx, S.mapClickCell.cy)
and ("Selected (%d,%d) · set it with the map buttons"):format(S.mapClickCell.cx, S.mapClickCell.cy)
or "Pinch to zoom · drag to pan · tap a cell",
vx0,
ly,
@@ -855,140 +972,7 @@ local function drawSection(S, Kit, x, y, w, h)
PAL.caption
)
end
if stacked then
return
end
end
-- ------------------------------------------------------ spawn inspector
local sx0 = sr.x
Kit.card(sx0, sr.y, sr.w, sr.h)
Kit.caption(sx0 + pad, sr.y + pad, "SPAWN POINTS")
local sTop = sr.y + pad + Kit.textHeight("caption") + 12 * s
local sInner = sr.w - 2 * pad
local pmap2, px2, py2 = playerPos(S)
local playerValue = pmap2 and ("%s (%d,%d)"):format(pmap2, px2, py2) or "unset"
local spawns
if Gen.of(S.save) == 3 then
local healMap = S.save.healMap
local healX = S.save.healX or 0
local healY = S.save.healY or 0
spawns = {
{
key = "PLAYER",
color = PAL.red,
value = playerValue,
set = function()
Ops.setPlayerHere(S)
end,
},
{
key = "LAST HEAL",
color = PAL.green,
value = healMap and ("%s (%d,%d)"):format(healMap, healX, healY) or "unset",
set = function()
Ops.setLastHeal(S)
end,
},
}
elseif Gen.of(S.save) == 2 then
spawns = {
{
key = "PLAYER",
color = PAL.red,
value = playerValue,
set = function()
Ops.setPlayerHere(S)
end,
},
{
key = "SPAWN",
color = PAL.green,
value = tostring(S.save.spawn or "SPAWN_HOME"),
set = function()
Ops.setLastHeal(S)
end,
},
}
else
local out = S.save.lastOutdoor
local heal = S.save.lastHeal
spawns = {
{
key = "PLAYER",
color = PAL.red,
value = playerValue,
set = function()
Ops.setPlayerHere(S)
end,
},
{
key = "LAST HEAL",
color = PAL.green,
value = heal and ("%s (%d,%d)"):format(heal.map, heal.x, heal.y) or "unset",
set = function()
Ops.setLastHeal(S)
end,
},
{
key = "LAST OUTDOOR",
color = PAL.yellow,
value = out and ("%s (%d,%d)"):format(out.id, out.x, out.y) or "unset",
set = function()
Ops.setLastOutdoor(S, map)
end,
},
}
end
local spawnH = math.max(80 * s, Kit.controlH() + 2 * Kit.textHeight("mono"))
local spawnTop = sTop
local spawnViewH = math.max(0, sr.y + sr.h - pad - spawnTop)
S.mapSpawnScroll = Kit.scrollPixels(
sx0 + pad,
spawnTop,
sInner,
spawnViewH,
S.mapSpawnScroll or 0,
#spawns * (spawnH + 8 * s) + 70 * s
)
Kit.pushClip(sx0 + pad, spawnTop, sInner, spawnViewH)
sTop = sTop - S.mapSpawnScroll
for i, sp in ipairs(spawns) do
local ry = sTop + (i - 1) * (spawnH + 8 * s)
Theme.row(sx0 + pad, ry, sInner, spawnH, 10 * s, 0.6)
Kit.text("tiny", sp.key, sx0 + pad + 12 * s, ry + 11 * s, sp.color)
local setW = 70 * s
if
Kit.button(
sx0 + pad + sInner - 12 * s - setW,
ry + 8 * s,
setW,
Kit.controlH(),
"Set here",
{ kind = "accent", font = "tiny", radius = 7 * s, enabled = S.mapClickCell ~= nil }
)
then
sp.set()
end
Kit.text(
"mono",
Kit.ellipsize("mono", sp.value, sInner - 24 * s),
sx0 + pad + 12 * s,
ry + spawnH - 10 * s - Kit.textHeight("mono"),
PAL.muted
)
end
local noteY = sTop + #spawns * (spawnH + 8 * s) + 6 * s
Kit.textWrapped(
"tiny",
"Select a cell in View, then set a spawn point here. Drag to pan; use the zoom controls to zoom.",
sx0 + pad,
noteY,
sInner,
PAL.caption
)
Kit.popClip()
end
function MapBrowser.draw(S, Kit, x, y, w, h)
@@ -999,7 +983,7 @@ function MapBrowser.draw(S, Kit, x, y, w, h)
require("src.world.gen2.MapPreview").clear(S._g2MapBaker)
end
S._mapWindowKey = windowKey
S._mapViewRect = nil
S._mapViewRect, S._mapOverlayRects = nil, nil
Motion.pages(S, Kit, "mapSection", x, y, w, h, drawSection)
if S._mapStacked and S.mapSection ~= "view" then S._mapViewRect = nil end
end
+21 -4
View File
@@ -11,10 +11,26 @@ local MonEditor = {}
-- species, cached for the process: the old panel called newImage every frame,
-- which re-decoded a PNG sixty times a second.
local spriteCache = {}
local SPINDA_CACHE_MAX = 16
local spindaKeys = {}
local function remember(key, value)
spriteCache[key] = value
if type(key) == "string" and key:find("^308:") then
spindaKeys[#spindaKeys + 1] = key
while #spindaKeys > SPINDA_CACHE_MAX do
spriteCache[table.remove(spindaKeys, 1)] = nil
end
end
end
function MonEditor.spriteKey(S, species, mon)
if not species then return nil end
local letter = mon and require("Ops").unownForm(S, mon)
if letter == nil and mon and Gen.ofState(S) == 3
and tonumber(species) == require("src.core.game3.pokemon").SPECIES_SPINDA then
return tostring(species) .. ":" .. tostring((tonumber(mon.personality) or 0) % 4294967296)
end
if letter == nil then return species end
return tostring(species) .. ":" .. letter
end
@@ -30,9 +46,10 @@ function MonEditor.sprite(S, species, mon)
local spId = tonumber(species) or (Pokemon.speciesFromName and Pokemon.speciesFromName(tostring(species)))
if spId then
local picId = letter ~= nil and Pokemon.picSpecies(spId, mon.personality) or spId
local pic = (Pokemon.frontPic and Pokemon.frontPic(picId)) or (Pokemon.icon and Pokemon.icon(picId))
local pic = (Pokemon.frontPic and Pokemon.frontPic(picId, nil, nil, mon and mon.personality))
or (Pokemon.icon and Pokemon.icon(picId))
if pic and pic.image then
spriteCache[key] = pic.image
remember(key, pic.image)
return pic.image
end
end
@@ -44,11 +61,11 @@ function MonEditor.sprite(S, species, mon)
path = require("src.core.gen2.Unown").formSprite(S.data.pokemon, letter) or path
end
if not path or not love.graphics.newImage then
spriteCache[key] = false
remember(key, false)
return nil
end
local ok, img = pcall(love.graphics.newImage, path)
spriteCache[key] = ok and img or false
remember(key, ok and img or false)
return ok and img or nil
end
+2 -1
View File
@@ -34,7 +34,7 @@ end
function Picker.commitFirst(S, Kit)
local hits = Picker.results(S)
if not hits[1] then
return Ops.say(S, "No move matches that")
return Ops.say(S, "No move matches that", "info")
end
local ok = commit(S, hits[1])
if ok then
@@ -80,6 +80,7 @@ function Picker.draw(S, Kit, width, height)
local closeW = PickerChrome.closeSize(Kit)
local captionH = Kit.textHeight("caption")
local headH = math.max(captionH, closeW)
PickerChrome.toastHeader(S, Kit, cx, cy, inner, headH)
Kit.caption(cx, cy + (headH - captionH) / 2, ("CHOOSE MOVE %d"):format(p.slot or 1))
if
Kit.iconButton(
+1
View File
@@ -49,6 +49,7 @@ local function drawRoster(S, Kit, x, y, listW, h)
-- "+ Add mon" / "Remove" pinned to the card bottom; the roster fills above.
local actH = Kit.controlH()
local actY = y + h - pad - actH
S.toastBottom = math.min(S.toastBottom or actY, actY)
local listTop = y + pad + Kit.textHeight("caption") + 12 * s
local listH = actY - 12 * s - listTop
+2 -1
View File
@@ -40,7 +40,7 @@ end
function Picker.commitFirst(S, Kit)
local hits = Picker.results(S)
if not hits[1] then
return Ops.say(S, "No species matches that")
return Ops.say(S, "No species matches that", "info")
end
local ok = commit(S, hits[1])
if ok then
@@ -86,6 +86,7 @@ function Picker.draw(S, Kit, width, height)
local closeW = PickerChrome.closeSize(Kit)
local captionH = Kit.textHeight("caption")
local headH = math.max(captionH, closeW)
PickerChrome.toastHeader(S, Kit, cx, cy, inner, headH)
Kit.caption(
cx,
cy + (headH - captionH) / 2,