mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 09:42:55 -04:00
+3
-1
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 "")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user