From 8c5d877bb9439253cf7b5e814dcc47c4b4867132 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Wed, 30 Sep 2026 15:47:06 -0400 Subject: [PATCH 1/2] various fixes --- src/core/Game3.lua | 1 + src/core/game3/battle/move_swap.lua | 74 +++ src/core/game3/battle/ui.lua | 79 ++- src/core/game3/battle_bridge.lua | 1 + src/core/game3/evolution.lua | 7 + src/core/game3/field.lua | 12 +- src/core/game3/field_moves.lua | 22 +- src/core/game3/item_use.lua | 29 +- src/core/game3/itemfinder.lua | 13 +- src/core/game3/link/battle.lua | 14 +- src/core/game3/party.lua | 2 +- src/core/game3/pokemon.lua | 6 + src/core/game3/profiles/emerald/ui.lua | 3 + src/core/game3/quest_log_recorder.lua | 4 +- src/core/game3/scripting/flags.lua | 1 + .../game3/scripting/natives_field_rse.lua | 14 + .../game3/scripting/natives_moveteach.lua | 7 +- src/import/gba/games/emerald.lua | 2 +- src/import/gba/games/emerald/text.lua | 1 + src/import/gba/move_relearner_rse_extract.lua | 33 ++ src/import/gba/plans/rse/ui.lua | 12 +- src/import/gba/rse/summary_chrome_extract.lua | 27 +- src/mods/Gen3Compat.lua | 10 +- src/ui/game3/bag_menu.lua | 10 +- src/ui/game3/coins_box.lua | 8 + src/ui/game3/egg_hatch.lua | 8 +- src/ui/game3/party_menu.lua | 21 +- src/ui/game3/rse/bag_menu.lua | 4 +- src/ui/game3/rse/move_relearner.lua | 453 ++++++++++++++++++ src/ui/game3/rse/shop_menu.lua | 8 + src/ui/game3/rse/summary_menu.lua | 15 +- src/ui/game3/sell_flow.lua | 72 ++- tests/drivers/battle_move_swap_common.lua | 183 +++++++ tests/drivers/em_bag_items_flags.lua | 109 +++++ tests/drivers/em_battle_move_swap.lua | 10 + tests/drivers/em_evolve_ability.lua | 39 ++ tests/drivers/em_itemfinder.lua | 70 +++ tests/drivers/em_link_trade_menu.lua | 57 +++ tests/drivers/em_move_relearner.lua | 146 ++++++ tests/drivers/em_shop_sell.lua | 113 +++++ tests/drivers/em_summary_info_page.lua | 67 +++ tests/drivers/em_teleport_vitamin.lua | 178 +++++++ tests/drivers/game3_battle_move_swap.lua | 10 + tests/drivers/game3_sell_flow_frlg.lua | 112 +++++ tests/emerald_move_relearner_test.lua | 65 +++ tests/engine/game3_battle_move_swap_test.lua | 73 +++ .../engine/game3_emerald_shared_keys_test.lua | 24 + 47 files changed, 2169 insertions(+), 60 deletions(-) create mode 100644 src/core/game3/battle/move_swap.lua create mode 100644 src/import/gba/move_relearner_rse_extract.lua create mode 100644 src/ui/game3/rse/move_relearner.lua create mode 100644 tests/drivers/battle_move_swap_common.lua create mode 100644 tests/drivers/em_bag_items_flags.lua create mode 100644 tests/drivers/em_battle_move_swap.lua create mode 100644 tests/drivers/em_evolve_ability.lua create mode 100644 tests/drivers/em_itemfinder.lua create mode 100644 tests/drivers/em_link_trade_menu.lua create mode 100644 tests/drivers/em_move_relearner.lua create mode 100644 tests/drivers/em_shop_sell.lua create mode 100644 tests/drivers/em_summary_info_page.lua create mode 100644 tests/drivers/em_teleport_vitamin.lua create mode 100644 tests/drivers/game3_battle_move_swap.lua create mode 100644 tests/drivers/game3_sell_flow_frlg.lua create mode 100644 tests/emerald_move_relearner_test.lua create mode 100644 tests/engine/game3_battle_move_swap_test.lua create mode 100644 tests/engine/game3_emerald_shared_keys_test.lua diff --git a/src/core/Game3.lua b/src/core/Game3.lua index fd37eb81..24cb6637 100644 --- a/src/core/Game3.lua +++ b/src/core/Game3.lua @@ -1169,6 +1169,7 @@ local SOFT_RESET = { { "src.ui.game3.daycare_menu", closeFlag("open") }, { "src.ui.game3.fame_checker", closeFlag("open") }, { "src.ui.game3.move_relearner", closeFlag("open") }, + { "src.ui.game3.rse.move_relearner", closeFlag("open") }, { "src.ui.game3.egg_hatch", closeFlag("open") }, { "src.ui.game3.evolution_scene", closeFlag("open") }, { "src.ui.game3.diploma", closeFlag("open") }, diff --git a/src/core/game3/battle/move_swap.lua b/src/core/game3/battle/move_swap.lua new file mode 100644 index 00000000..b6da1e3d --- /dev/null +++ b/src/core/game3/battle/move_swap.lua @@ -0,0 +1,74 @@ +local MoveSwap = {} + +local function filled(mv) + return mv ~= nil and mv ~= 0 and mv ~= "" +end + +function MoveSwap.moveCount(mon) + local n = 0 + for i = 1, 4 do + if not filled(mon and mon.moves and mon.moves[i]) then break end + n = n + 1 + end + return n +end + +-- pokeemerald/src/battle_controller_player.c:601 +function MoveSwap.canStart(st, battler) + if not st or st.link or not battler or not battler.mon then return false end + return MoveSwap.moveCount(battler.mon) > 1 +end + +-- pokeemerald/src/battle_controller_player.c:604 +function MoveSwap.initialCursor(current) + if current ~= 0 then return 0 end + return current + 1 +end + +-- pokeemerald/src/battle_controller_player.c:795 +function MoveSwap.step(cursor, dir, n) + if dir == "left" then + if cursor % 2 == 1 then return cursor - 1 end + elseif dir == "right" then + if cursor % 2 == 0 and cursor + 1 < n then return cursor + 1 end + elseif dir == "up" then + if cursor >= 2 then return cursor - 2 end + elseif dir == "down" then + if cursor < 2 and cursor + 2 < n then return cursor + 2 end + end + return cursor +end + +local function swap_field(t, a, b) + if type(t) == "table" then t[a], t[b] = t[b], t[a] end +end + +-- pokeemerald/src/battle_controller_player.c:676 +function MoveSwap.apply(battler, slotA, slotB) + if not battler or not battler.mon or slotA == slotB then return false end + local Pokemon = require("src.core.game3.pokemon") + local State = require("src.core.game3.battle.state") + local party = State.partyMon(battler) + if battler._partyMon then + local proxy = battler.mon + swap_field(rawget(proxy, "moves"), slotA, slotB) + swap_field(rawget(proxy, "pp"), slotA, slotB) + swap_field(battler.permanentSlots, slotA, slotB) + swap_field(battler.sketched, slotA, slotB) + if not battler.transformed and party then + Pokemon.swapMoves(party, slotA, slotB) + end + else + Pokemon.swapMoves(party or battler.mon, slotA, slotB) + end + for _, key in ipairs({ "expLockedSlot", "expEncoreSlot" }) do + if battler[key] == slotA then + battler[key] = slotB + elseif battler[key] == slotB then + battler[key] = slotA + end + end + return true +end + +return MoveSwap diff --git a/src/core/game3/battle/ui.lua b/src/core/game3/battle/ui.lua index 51457039..2cc34ebf 100644 --- a/src/core/game3/battle/ui.lua +++ b/src/core/game3/battle/ui.lua @@ -224,6 +224,7 @@ function Ui.reset(opts) Ui._actionCursor = {} Ui._moveCursor = {} Ui._moveCursorMon = {} + Ui._swap = nil Ui._target = nil Ui._bounce = { hb = {}, mon = {} } Ui._preview = nil @@ -961,8 +962,46 @@ local function move_count(mon) return n end +-- pokeemerald/src/battle_controller_player.c:599 +local function move_swap_input(input, st, battler, id) + local MoveSwap = require("src.core.game3.battle.move_swap") + local sw = Ui._swap + local cur = (Ui._moveIndex or 1) - 1 + if not sw then + if input:wasPressed("select") and MoveSwap.canStart(st, battler) then + Ui._swap = { cursor = MoveSwap.initialCursor(cur) } + return true + end + return false + end + if input:wasPressed("a") or input:wasPressed("select") then + play_select() + if sw.cursor ~= cur then MoveSwap.apply(battler, cur + 1, sw.cursor + 1) end + Ui._moveIndex = sw.cursor + 1 + if id then Ui._moveCursor[id] = Ui._moveIndex end + Ui._swap = nil + elseif input:wasPressed("b") then + play_select() + Ui._swap = nil + else + local n = MoveSwap.moveCount(battler and battler.mon) + for _, dir in ipairs({ "left", "right", "up", "down" }) do + if input:wasPressed(dir) then + local nc = MoveSwap.step(sw.cursor, dir, n) + if nc ~= sw.cursor then + sw.cursor = nc + play_select() + end + break + end + end + end + return true +end + -- pokefirered/src/battle_main.c:2380 local function open_move_menu() + Ui._swap = nil if is_double() then local id = Ui._active or 0 local battler = active_battler() @@ -1519,6 +1558,7 @@ local function handle_double_input(input) return true elseif Ui._mode == "moves" then local b = active_battler(st) + if move_swap_input(input, st, b, id) then return true end local n = move_count(b and b.mon) local c = (Ui._moveIndex or 1) - 1 local nc = c @@ -1656,6 +1696,7 @@ function Ui.handleInput(input) -- Input owned by BagMenu / PartyMenu via Battle.update return true elseif Ui._mode == "moves" then + if move_swap_input(input, Ui._st, Ui._st and Ui._st.player, nil) then return true end -- pokefirered/src/battle_controller_player.c:526 local idx, moved = grid_nav(Ui._moveIndex, input, move_count(Ui._st and Ui._st.player and Ui._st.player.mon)) if moved then @@ -2181,20 +2222,45 @@ local function draw_action_menu_rse(st) end end +local function swap_cursor_colors(variant, base) + local clear = { 0, 0, 0, 0 } + if variant == 27 then return { fg = base.fg, shadow = clear, bg = base.bg } end + return { fg = base.shadow, shadow = clear, bg = base.bg } +end + +-- pokeemerald/src/battle_controller_player.c:608 +local function draw_move_cursors(pos_of, base) + local cur = Ui._moveIndex - 1 + local sw = Ui._swap + local p = pos_of(cur) + if not sw or sw.cursor == cur then + Window.cursorPx(p[1], p[2], { colors = base }) + return + end + Window.cursorPx(p[1], p[2], { colors = swap_cursor_colors(29, base) }) + local q = pos_of(sw.cursor) + Window.cursorPx(q[1], q[2], { colors = swap_cursor_colors(27, base) }) +end + -- pokeemerald/src/battle_controller_player.c:1456 local function draw_move_menu_rse(st) local W = BattleChrome.WIN local ab = st and (is_double(st) and active_battler(st) or st.player) local mon = ab and ab.mon - local c = Ui._moveIndex - 1 local _, cy = BattleChrome.textOrigin(W.MOVE_NAME_1) - Window.cursorPx(8 * (9 * (c % 2) + 1), cy + 16 * math.floor(c / 2), { colors = rse_default_colors() }) + draw_move_cursors(function(c) + return { 8 * (9 * (c % 2) + 1), cy + 16 * math.floor(c / 2) } + end, rse_default_colors()) for i = 1, 4 do local mv = mon and mon.moves and mon.moves[i] local label = "-" if mv and mv ~= 0 and mv ~= "" then label = Moves.displayName(mv) end rse_text(W.MOVE_NAME_1 + i - 1, label) end + if Ui._swap then + rse_text(W.SWITCH_PROMPT, RomText.plain("gText_BattleSwitchWhich")) + return + end local slot = Ui._moveIndex local mv = mon and mon.moves and mon.moves[slot] if mv and mv ~= 0 and mv ~= "" then @@ -2251,9 +2317,7 @@ local function draw_move_menu(st) { 8, 122 }, { 80, 122 }, { 8, 138 }, { 80, 138 }, } - local c = Ui._moveIndex - 1 - local cp = cursorPos[c + 1] or cursorPos[1] - Window.cursorPx(cp[1], cp[2], { colors = FrlgFont.COLOR.NORMAL }) + draw_move_cursors(function(c) return cursorPos[c + 1] or cursorPos[1] end, FrlgFont.COLOR.NORMAL) for i = 1, 4 do local mv = mon and mon.moves and mon.moves[i] -- pokefirered/src/data/text/move_names.h:2 @@ -2263,6 +2327,11 @@ local function draw_move_menu(st) end draw_menu_text(label, positions[i][1], positions[i][2], { small = true, colors = FrlgFont.COLOR.NORMAL }) end + if Ui._swap then + draw_menu_text(RomText.plain("gText_BattleSwitchWhich"), 168, 122, + { small = false, colors = FrlgFont.COLOR.NORMAL }) + return + end local slot = Ui._moveIndex local mv = mon and mon.moves and mon.moves[slot] if mv and mv ~= 0 and mv ~= "" then diff --git a/src/core/game3/battle_bridge.lua b/src/core/game3/battle_bridge.lua index b72ab11c..7b0fb699 100644 --- a/src/core/game3/battle_bridge.lua +++ b/src/core/game3/battle_bridge.lua @@ -218,6 +218,7 @@ local function writeback(session, battleParty, remap, result, save, opts) exp = src.exp, pp = src.pp, maxPp = src.maxPp, + ppBonusesPacked = src.ppBonusesPacked, moves = src.moves, species = src.species or src.speciesId, speciesId = src.speciesId or src.species, diff --git a/src/core/game3/evolution.lua b/src/core/game3/evolution.lua index 327290f3..96f32cee 100644 --- a/src/core/game3/evolution.lua +++ b/src/core/game3/evolution.lua @@ -304,6 +304,13 @@ function Evolution.apply(mon, newSpecies, session, bag, via) -- 2. Nickname update Evolution.renameMon(mon, preSpecies, newSpecies) + -- pokeemerald/src/pokemon.c:4556 GetMonAbility + local pair = Pokemon.abilities(newSpecies) + local slot = tonumber(mon.abilityNum) + local ability = slot and pair[slot + 1] + if not ability or ability == 0 then ability = Pokemon.abilityId(newSpecies, mon.personality) end + mon.ability, mon.abilityId = ability, ability + -- 3. Recalculate stats & handle HP delta Pokemon.applyStats(mon) local newMax = tonumber(mon.maxHp) or oldMax diff --git a/src/core/game3/field.lua b/src/core/game3/field.lua index 96a71b43..ea1e3bd8 100644 --- a/src/core/game3/field.lua +++ b/src/core/game3/field.lua @@ -512,6 +512,11 @@ end -- pokefirered/data/scripts/itemfinder.inc:1 function Field.digUpUnderfootItem(game, hidden) + if require("src.core.game3.profile").family(Field._session) == "rse" then + -- pokeemerald/src/item_use.c:597 + require("src.ui.game3.message").close() + return false + end if not hidden then return false end local Flags = require("src.core.game3.scripting.flags") local flag = hidden_flag(hidden) @@ -552,14 +557,14 @@ function Field.useItemfinder(session, showOWMessage) }) if not result then - local text = RomText.box("gText_NopeTheresNoResponse") + local text = RomText.box(Itemfinder.textKey("nothing", session)) if showOWMessage then -- pokefirered/src/itemfinder.c:150 Message.show(text, { session = session, done = function() Message.close() end }) end return false, "itemfinder", text, nil end - local key = result.underfoot and "gText_ItemfinderShakingWildly" or "gText_ItemfinderResponding" + local key = Itemfinder.textKey(result.underfoot and "onTop" or "nearby", session) local text = RomText.box(key) if showOWMessage then Field.lock() @@ -1849,8 +1854,7 @@ function Field.clearTempFieldEventData(game, mapId) local Flags = require("src.core.game3.scripting.flags") local Space = package.loaded["src.core.game3.scripting.space"] local session = Field._session - local ids = {} - for i = 1, #FieldMoves.TEMP_SYS_FLAGS do ids[i] = FieldMoves.TEMP_SYS_FLAGS[i] end + local ids = FieldMoves.tempSysFlags() local data = game and game.data and game.data.maps local def = mapId and data and data[mapId] -- pokefirered/src/overworld.c:803 diff --git a/src/core/game3/field_moves.lua b/src/core/game3/field_moves.lua index 08454d6a..cfe29d7f 100644 --- a/src/core/game3/field_moves.lua +++ b/src/core/game3/field_moves.lua @@ -71,6 +71,8 @@ local FRLG_SYS_FLAGS = { -- pokeemerald/include/constants/flags.h:1398 local RSE_SYS_FLAGS = { + WHITE_FLUTE_ACTIVE = "FLAG_SYS_ENC_UP_ITEM", + BLACK_FLUTE_ACTIVE = "FLAG_SYS_ENC_DOWN_ITEM", USE_STRENGTH = "FLAG_SYS_USE_STRENGTH", FLASH_ACTIVE = "FLAG_SYS_USE_FLASH", } @@ -85,9 +87,23 @@ FieldMoves.SYS_FLAGS = setmetatable({}, { }) -- src/event_data.c:49 -FieldMoves.TEMP_SYS_FLAGS = { - 0x803, 0x804, 0x805, -} +local FRLG_TEMP_SYS_FLAGS = { "WHITE_FLUTE_ACTIVE", "BLACK_FLUTE_ACTIVE", "USE_STRENGTH" } + +-- pokeemerald/src/event_data.c:39 +local RSE_TEMP_SYS_FLAGS = { "WHITE_FLUTE_ACTIVE", "BLACK_FLUTE_ACTIVE", "USE_STRENGTH", "FLAG_SYS_CTRL_OBJ_DELETE", + "FLAG_NURSE_UNION_ROOM_REMINDER" } + +function FieldMoves.tempSysFlags() + local P = activeProfile() + local rse = P.family == "rse" + local IDS = rse and Flags.forVersion(P.id).IDS or nil + local out = {} + for _, key in ipairs(rse and RSE_TEMP_SYS_FLAGS or FRLG_TEMP_SYS_FLAGS) do + local id = FieldMoves.SYS_FLAGS[key] or (IDS and IDS[key]) + if id then out[#out + 1] = id end + end + return out +end -- Graphics IDs for interactable field objects local FRLG_GFX_IDS = { diff --git a/src/core/game3/item_use.lua b/src/core/game3/item_use.lua index 93ba1fd5..c0fefdae 100644 --- a/src/core/game3/item_use.lua +++ b/src/core/game3/item_use.lua @@ -650,6 +650,12 @@ local VITAMIN_STAT_TEXT = { spe = "gText_ItemEffect_Speed", spa = "gText_ItemEffect_SpAtk", spd = "gText_ItemEffect_SpDef", } +-- pokeemerald/src/party_menu.c:4338 +local VITAMIN_STAT_TEXT_RSE = { + hp = "gText_HP3", atk = "gText_Attack3", def = "gText_Defense3", + spe = "gText_Speed2", spa = "gText_SpAtk3", spd = "gText_SpDef3", +} + function ItemUse.useVitamin(session, mon, itemId) if not mon then return false, "none", no_pokemon_text() end local num = ItemsData.toNumericId(itemId) or tonumber(itemId) @@ -667,7 +673,8 @@ function ItemUse.useVitamin(session, mon, itemId) end Pokemon.itemFriendship(mon, Pokemon.VITAMIN_FRIENDSHIP_CHANGE, { mapSec = Pokemon.currentMapSec(session) }) - local t = mon_text("gText_PkmnBaseVar2StatIncreased", mon, RomText.plain(VITAMIN_STAT_TEXT[key])) + local statTextKey = require("src.core.game3.profile").family(session) == "rse" and VITAMIN_STAT_TEXT_RSE or VITAMIN_STAT_TEXT + local t = mon_text("gText_PkmnBaseVar2StatIncreased", mon, RomText.plain(statTextKey[key])) return true, "vitamin", t end @@ -874,9 +881,6 @@ local ITEM_TEACHY_TV = 366 -- pokefirered/include/constants/items.h:435 ITEM_FAME_CHECKER local ITEM_FAME_CHECKER = 363 local ITEM_AWAKENING = 17 --- pokefirered/include/constants/flags.h:1330 -local FLAG_SYS_WHITE_FLUTE_ACTIVE = 0x803 -local FLAG_SYS_BLACK_FLUTE_ACTIVE = 0x804 -- pokefirered/include/constants/songs.h:346 MUS_POKE_FLUTE local MUS_POKE_FLUTE = 338 @@ -1010,19 +1014,20 @@ end -- pokefirered/src/item_use.c:582 FieldUseFunc_BlackWhiteFlute function ItemUse.useBlackWhiteFlute(session, num) + local FieldMoves = require("src.core.game3.field_moves") local ctx = { playerName = tostring((session and (session.name or session.playerName)) or ""), stringVars = { [2] = ItemsData.displayName(num) }, } local text if num == ITEM_WHITE_FLUTE then - sys_flag(session, FLAG_SYS_WHITE_FLUTE_ACTIVE, true) - sys_flag(session, FLAG_SYS_BLACK_FLUTE_ACTIVE, false) + sys_flag(session, FieldMoves.SYS_FLAGS.WHITE_FLUTE_ACTIVE, true) + sys_flag(session, FieldMoves.SYS_FLAGS.BLACK_FLUTE_ACTIVE, false) -- pokefirered/src/item_use.c:590 text = RomText.box("gText_UsedVar2WildLured", ctx) else - sys_flag(session, FLAG_SYS_BLACK_FLUTE_ACTIVE, true) - sys_flag(session, FLAG_SYS_WHITE_FLUTE_ACTIVE, false) + sys_flag(session, FieldMoves.SYS_FLAGS.BLACK_FLUTE_ACTIVE, true) + sys_flag(session, FieldMoves.SYS_FLAGS.WHITE_FLUTE_ACTIVE, false) -- pokefirered/src/item_use.c:599 text = RomText.box("gText_UsedVar2WildRepelled", ctx) end @@ -1044,6 +1049,14 @@ local function useField(session, bag, id, partySlot, moveSlot) if not info then return false, "unknown", Strings("Unknown item.") end local use = ItemsData.fieldUseKind(id) + if require("src.core.game3.profile").family(session) == "rse" then + local n = ItemsData.toNumericId(id) or tonumber(id) + if n == ITEM_POKE_FLUTE or n == ItemsData.ITEM_TM_CASE or n == ItemsData.ITEM_BERRY_POUCH then + -- pokeemerald/src/item_use.c:150 ItemUseOutOfBattle_CannotUse + return false, "none", not_the_time(session) + end + end + if info.fieldUseName == "ItemUseOutOfBattle_PokeblockCase" then -- pokeemerald/src/item_use.c:609 require("src.core.game3.rse.pokeblock").openCase(session, {}) diff --git a/src/core/game3/itemfinder.lua b/src/core/game3/itemfinder.lua index 9d851ea3..abdd9566 100644 --- a/src/core/game3/itemfinder.lua +++ b/src/core/game3/itemfinder.lua @@ -147,6 +147,17 @@ function Itemfinder.start(task) Itemfinder._task = task end +local TEXT_KEYS = { + frlg = { nothing = "gText_NopeTheresNoResponse", nearby = "gText_ItemfinderResponding", onTop = "gText_ItemfinderShakingWildly" }, + rse = { nothing = "gText_ItemFinderNothing", nearby = "gText_ItemFinderNearby", onTop = "gText_ItemFinderOnTop" }, +} + +-- pokeemerald/src/item_use.c:295 +function Itemfinder.textKey(kind, session) + local family = require("src.core.game3.profile").family(session) + return (TEXT_KEYS[family] or TEXT_KEYS.frlg)[kind] +end + function Itemfinder.isActive() return Itemfinder._task ~= nil end @@ -167,7 +178,7 @@ function Itemfinder.runTask() if t.timer % 25 == 0 then if t.remaining == 0 then t.phase = "message" - local key = t.result.underfoot and "gText_ItemfinderShakingWildly" or "gText_ItemfinderResponding" + local key = Itemfinder.textKey(t.result.underfoot and "onTop" or "nearby") t.onMessage(key, function() Itemfinder._task = nil Itemfinder._sprites = {} diff --git a/src/core/game3/link/battle.lua b/src/core/game3/link/battle.lua index e16544c5..33289e50 100644 --- a/src/core/game3/link/battle.lua +++ b/src/core/game3/link/battle.lua @@ -1442,9 +1442,17 @@ function LB.finish(result) bumpTrainerCard(s, recorded) LB.addOpponentRecord(s, LB.peer and LB.peer.name, LB.peer and LB.peer.trainerId, recorded) -- pokefirered/src/cable_club.c:782 - local okF, TFC = pcall(require, "src.core.game3.trainer_fan_club") - if okF and TFC and TFC.updateTrainerFansAfterLinkBattle then - TFC.updateTrainerFansAfterLinkBattle(s, ctx, recorded) + if require("src.core.game3.profile").family(s) == "rse" then + -- pokeemerald/src/field_specials.c:4244 + local okR, FieldRse = pcall(require, "src.core.game3.scripting.natives_field_rse") + if okR and FieldRse.updateTrainerFansAfterLinkBattle then + FieldRse.updateTrainerFansAfterLinkBattle(recorded == LB.B_OUTCOME.WON) + end + else + local okF, TFC = pcall(require, "src.core.game3.trainer_fan_club") + if okF and TFC and TFC.updateTrainerFansAfterLinkBattle then + TFC.updateTrainerFansAfterLinkBattle(s, ctx, recorded) + end end end end diff --git a/src/core/game3/party.lua b/src/core/game3/party.lua index 5827bfe5..c9c77572 100644 --- a/src/core/game3/party.lua +++ b/src/core/game3/party.lua @@ -49,7 +49,7 @@ function Party.applyBattleFields(opaqueMon, fields) "happiness", "friendship", "evs", "pokerus", "item", "heldItem", "species", "speciesId", "name", "growthRate", "attack", "defense", "speed", "spAtk", "spDef", - "atk", "def", "spe", "spa", "spd", + "atk", "def", "spe", "spa", "spd", "ppBonusesPacked", }) do if fields[key] ~= nil then opaqueMon[key] = fields[key] end end diff --git a/src/core/game3/pokemon.lua b/src/core/game3/pokemon.lua index d542437b..10f30baf 100644 --- a/src/core/game3/pokemon.lua +++ b/src/core/game3/pokemon.lua @@ -1311,6 +1311,12 @@ function Pokemon.swapMoves(mon, slotA, slotB) mon.pp[slotB] = ppA end + if type(mon.maxPp) == "table" then + local maxA = mon.maxPp[slotA] + mon.maxPp[slotA] = mon.maxPp[slotB] + mon.maxPp[slotB] = maxA + end + -- 4. If parallel array mon.ppBonuses / mon.ppBonus / mon.ppUp exists if type(mon.ppBonuses) == "table" then local bA = mon.ppBonuses[slotA] diff --git a/src/core/game3/profiles/emerald/ui.lua b/src/core/game3/profiles/emerald/ui.lua index 0a5986db..fd9eb6b6 100644 --- a/src/core/game3/profiles/emerald/ui.lua +++ b/src/core/game3/profiles/emerald/ui.lua @@ -82,6 +82,9 @@ return { gText_MonLearnedMove = "gText_MoveRelearnerPkmnLearnedMove", gText_1_2_and_Poof = "gText_MoveRelearnerAndPoof", gText_StopLearningMove = "gText_MoveRelearnerStopTryingToTeachMove", + gText_MonIsTryingToLearnMove = "gText_MoveRelearnerPkmnTryingToLearnMove", + gText_WhichMoveShouldBeForgotten = "gText_MoveRelearnerWhichMoveToForget", + gText_MonForgotOldMoveAndMonLearnedNewMove = "gText_MoveRelearnerPkmnForgotMoveAndLearnedNew", }, -- pokeemerald/src/berry_powder.c:223 berryPowderBox = { left = 1, top = 1, width = 7, height = 4, title = "gText_Powder", titleX = 0, titleY = 1, diff --git a/src/core/game3/quest_log_recorder.lua b/src/core/game3/quest_log_recorder.lua index 0c451dcb..5abfe1e4 100644 --- a/src/core/game3/quest_log_recorder.lua +++ b/src/core/game3/quest_log_recorder.lua @@ -3,7 +3,8 @@ local Q=require('src.core.game3.quest_log') local R={} function R.location(game,session) local def=game and game.data and game.data.maps and game.data.maps[session.map] - local info=require("src.import.gba.map_sections_extract").getInfo(def and def.regionMapSectionId,session.map,0) + local ok,info=pcall(require("src.import.gba.map_sections_extract").getInfo,def and def.regionMapSectionId,session.map,0) + if not ok then info=nil end if info and info.name and info.name~="???" then return info.name end return tostring(session.map or ''):gsub('^FR_',''):gsub('_',' ') end @@ -46,6 +47,7 @@ function R.tiles(game,session,frame) return R.fillTiles(game,session,frame,{}) end function R.event(session,key,args) + if not require('src.core.game3.field_modules').enabled('questLog',session) then return end local Runtime=package.loaded['src.core.game3.runtime'] -- Ignore simulations/tests and sessions that aren't the active game. if not session or not Runtime or type(Runtime.isActive) ~= "function" or not Runtime.isActive() or (Runtime.getSession and Runtime.getSession() ~= session) then return end diff --git a/src/core/game3/scripting/flags.lua b/src/core/game3/scripting/flags.lua index 260311f7..fb7f13c9 100644 --- a/src/core/game3/scripting/flags.lua +++ b/src/core/game3/scripting/flags.lua @@ -284,6 +284,7 @@ end -- Do not force-hide lab Oak (43) — clearflag during the lead warp must stick. function Flags.ensurePalletOakHidden(store) if not store then return end + if require("src.core.game3.profile").family() ~= "frlg" then return end local sceneVar = Flags.VAR_IDS.MAP_SCENE_PALLET_TOWN_OAK or 0x4050 local scene = Flags.getVar(store, nil, sceneVar) if scene ~= 0 then return end diff --git a/src/core/game3/scripting/natives_field_rse.lua b/src/core/game3/scripting/natives_field_rse.lua index a1e30915..74975b81 100644 --- a/src/core/game3/scripting/natives_field_rse.lua +++ b/src/core/game3/scripting/natives_field_rse.lua @@ -1278,6 +1278,20 @@ function FieldRse.tryGainNewFanFromCounter(incrementId) return bit.band(fanBits(), FANCLUB_COUNTER) end +-- pokeemerald/src/field_specials.c:4244 +function FieldRse.updateTrainerFansAfterLinkBattle(won) + if (tonumber(Rse.var("VAR_LILYCOVE_FAN_CLUB_STATE")) or 0) ~= 2 then return end + if fanFlag(FANCLUB_GOT_FIRST_FANS) then + FieldRse.tryLoseFansFromPlayTime() + Rse.setVar("VAR_FANCLUB_LOSE_FAN_TIMER", playTimeHours()) + end + if won then + FieldRse.playerGainRandomTrainerFan() + else + FieldRse.playerLoseRandomTrainerFan() + end +end + -- pokeemerald/src/field_specials.c:4170 local FAN_NAME_SOURCES = { [9] = { 0, 3 }, [10] = { 0, 1 }, [11] = { 1, 0 }, [12] = { 0, 4 }, [13] = { 1, 5 }, diff --git a/src/core/game3/scripting/natives_moveteach.lua b/src/core/game3/scripting/natives_moveteach.lua index 9032ec41..00b25b23 100644 --- a/src/core/game3/scripting/natives_moveteach.lua +++ b/src/core/game3/scripting/natives_moveteach.lua @@ -140,7 +140,12 @@ MoveTeach.BY_NAME = { -- pokefirered/src/learn_move.c:367 TeachMoveRelearnerMove = function(ctx, adapters) local mon, session = chosenMon(ctx) - local okUi, MoveRelearner = pcall(require, "src.ui.game3.move_relearner") + local screen = "src.ui.game3.move_relearner" + if require("src.core.game3.profile").family(session) == "rse" then + -- pokeemerald/src/move_relearner.c:373 + screen = "src.ui.game3.rse.move_relearner" + end + local okUi, MoveRelearner = pcall(require, screen) if not (mon and okUi and type(MoveRelearner) == "table" and MoveRelearner.show) then varSet(ctx, VAR_0x8004, 0) return false diff --git a/src/import/gba/games/emerald.lua b/src/import/gba/games/emerald.lua index e65eb9da..cc9794f9 100644 --- a/src/import/gba/games/emerald.lua +++ b/src/import/gba/games/emerald.lua @@ -3,7 +3,7 @@ local VersionsRse = require("src.import.gba.versions_rse") local V = VersionsRse.new("emerald") local sym, count = V.sym, V.count -V.CACHE_VERSION = 9 +V.CACHE_VERSION = 11 V.NATIVE_VERSION = 2 V.OW_VERSION = 1 V.ANIM_VERSION = 1 diff --git a/src/import/gba/games/emerald/text.lua b/src/import/gba/games/emerald/text.lua index 8889b967..43e01387 100644 --- a/src/import/gba/games/emerald/text.lua +++ b/src/import/gba/games/emerald/text.lua @@ -19,6 +19,7 @@ return function(V) end V.NAMED_TEXTS = offsets(Text.NAMED_TEXTS) + V.NAMED_TEXTS.sText_CommunicationStandby = S.off("trade.o:sText_CommunicationStandby") V.NAMED_BATTLE_TEXTS = offsets(Text.NAMED_BATTLE_TEXTS) local tables = {} diff --git a/src/import/gba/move_relearner_rse_extract.lua b/src/import/gba/move_relearner_rse_extract.lua new file mode 100644 index 00000000..d755c0e5 --- /dev/null +++ b/src/import/gba/move_relearner_rse_extract.lua @@ -0,0 +1,33 @@ +local K = require("src.import.gba.rse.boot_gfx") + +local M = {} + +M.SUB = "rse/move_relearner" +M.FILES = { "hearts.png" } +M.REQUIRED = K.required(M.SUB, M.FILES) + +-- pokeemerald/src/move_relearner.c:149 +M.HEARTS = { "appealEmpty", "appealFull", "jamEmpty", "jamFull" } +-- pokeemerald/src/move_relearner.c:286 +local FIRST_HEART_TILE = 8 + +function M.run(rom, cache, opts) + local c = K.context(rom, cache, opts, M.SUB) + -- pokeemerald/src/move_relearner.c:188 + local tiles = c:raw("move_relearner.o:sUI_Tiles") + local pal = c:pal("move_relearner.o:sUI_Pal", 16) + local hearts = c:strip("hearts", tiles, 8, 8, #M.HEARTS, pal, 4, FIRST_HEART_TILE) + local frames = {} + for i, name in ipairs(M.HEARTS) do frames[name] = i - 1 end + hearts.names = frames + return true, c:finish({ + screen = "move_relearner", + hearts = hearts, + }) +end + +function M.ready(cache, cacheRoot) + return K.ready(M.SUB, cache, cacheRoot) +end + +return M diff --git a/src/import/gba/plans/rse/ui.lua b/src/import/gba/plans/rse/ui.lua index 353213fe..5861d5d9 100644 --- a/src/import/gba/plans/rse/ui.lua +++ b/src/import/gba/plans/rse/ui.lua @@ -18,7 +18,15 @@ return { { name = "multichoice_extract", label = "Dialog Menus" }, }, }, + { + id = "ui_relearner", + run = "steps", + weight = 0.01, + steps = { + { name = "move_relearner_rse_extract", label = "Move Relearner" }, + }, + }, }, - sequential = { "ui_wild", "ui_places" }, - dirs = { "/region_map", "/scripts", "/chrome/map_popup" }, + sequential = { "ui_wild", "ui_places", "ui_relearner" }, + dirs = { "/region_map", "/scripts", "/chrome/map_popup", "/rse/move_relearner" }, } diff --git a/src/import/gba/rse/summary_chrome_extract.lua b/src/import/gba/rse/summary_chrome_extract.lua index 629e089d..68881bcd 100644 --- a/src/import/gba/rse/summary_chrome_extract.lua +++ b/src/import/gba/rse/summary_chrome_extract.lua @@ -8,7 +8,7 @@ M.PAGES = { "info", "info_egg", "skills", "battle_moves", "contest_moves" } M.FILES = { "info.png", "info_egg.png", "skills.png", "battle_moves.png", "contest_moves.png", - "move_types.png", "move_select.png", "status.png", "tiles.png", "markings.png", + "move_types.png", "move_select.png", "status.png", "status_plate.png", "tiles.png", "markings.png", } -- pokeemerald/src/pokemon_summary_screen.c:2338 @@ -54,6 +54,19 @@ local function bytes(c, name) return out end +-- pokeemerald/src/pokemon_summary_screen.c:2405 +local function clearStatusPlate(map) + local out = {} + for i = 1, #map do out[i] = map:sub(i, i) end + for row = 18, 19 do + for col = 0, 9 do + local o = (row * 32 + col) * 2 + out[o + 1], out[o + 2] = "\1", "\0" + end + end + return table.concat(out) +end + function M.run(rom, cache, opts) local c = K.context(rom, cache, opts, M.SUB) local gfx = c:lz("gSummaryScreen_Gfx") @@ -62,7 +75,9 @@ function M.run(rom, cache, opts) c:pal("gPPTextPalette", 15, pal, 8 * 16 + 1) local layers = {} for _, key in ipairs(M.PAGES) do - local idx, W, H = K.bakeText(gfx, c:lz(TILEMAPS[key]), 30, 20) + local map = c:lz(TILEMAPS[key]) + if key == "info" or key == "info_egg" then map = clearStatusPlate(map) end + local idx, W, H = K.bakeText(gfx, map, 30, 20) layers[key] = c:layer({ key = key }, idx, W, H, pal) end layers.info.backdrop = pal[0] @@ -115,6 +130,13 @@ function M.run(rom, cache, opts) local selSheet, selW, selH = K.stack(selFrames, 16, 16) c:png("move_select.png", selW, selH, selSheet, selPal, true) + -- pokeemerald/src/pokemon_summary_screen.c:369 + local plateMap = string.char( + 4, 0, 4, 0, 4, 0, 4, 0, 4, 0, 4, 0, 0x17, 8, 0x17, 8, 0x17, 8, 0x0b, 0, + 4, 8, 4, 8, 4, 8, 4, 8, 4, 8, 4, 8, 0x17, 0, 0x17, 0, 0x17, 0, 0x0b, 8) + local plateIdx, PW, PH = K.bakeText(gfx, plateMap, 10, 2, { linear = true, mapWidth = 10 }) + c:png("status_plate.png", PW, PH, plateIdx, pal, true) + -- pokeemerald/src/pokemon_summary_screen.c:1367 local stPal = c:pal("gStatusPal_Icons", 16, nil, 0, true) local stGfx = c:lz("gStatusGfx_Icons") @@ -140,6 +162,7 @@ function M.run(rom, cache, opts) tiles = { png = c:path("tiles.png"), index = tileIndex }, moveTypes = { png = c:path("move_types.png"), w = 32, h = 16, count = typeCount }, moveSelect = { png = c:path("move_select.png"), w = 16, h = 16, count = #selFrames }, + statusPlate = { png = c:path("status_plate.png"), x = 0, y = 144 }, status = { png = c:path("status.png"), w = 32, h = 8, count = #stFrames }, palette = K.palList(pal, 0, 144), windows = windows(c, "sSummaryTemplate"), diff --git a/src/mods/Gen3Compat.lua b/src/mods/Gen3Compat.lua index c8f179ff..694ecf38 100644 --- a/src/mods/Gen3Compat.lua +++ b/src/mods/Gen3Compat.lua @@ -308,7 +308,10 @@ local function flagId(name) local n = tonumber(name) if n then return n end local Flags = g3("scripting.flags") - return Flags and Flags.IDS and Flags.IDS[name] or nil + if not Flags then return nil end + local ok, t = pcall(Flags.active, session()) + local ids = ok and t and t.IDS or Flags.IDS + return ids and ids[name] or nil end local function varId(name) @@ -317,7 +320,10 @@ local function varId(name) local n = tonumber(name) if n then return n end local Flags = g3("scripting.flags") - return Flags and Flags.VAR_IDS and Flags.VAR_IDS[name] or nil + if not Flags then return nil end + local ok, t = pcall(Flags.active, session()) + local ids = ok and t and t.VAR_IDS or Flags.VAR_IDS + return ids and ids[name] or nil end function Gen3Compat.getFlag(name) diff --git a/src/ui/game3/bag_menu.lua b/src/ui/game3/bag_menu.lua index 7792b245..4acc10b4 100644 --- a/src/ui/game3/bag_menu.lua +++ b/src/ui/game3/bag_menu.lua @@ -583,7 +583,8 @@ local function begin_sell(row) clamp_cursor() reshow() end - if num == ItemsData.ITEM_TM_CASE then + local frlgCases = require("src.core.game3.profile").family(BagMenu._session) ~= "rse" + if frlgCases and num == ItemsData.ITEM_TM_CASE then -- src/item_menu.c:1825 GoToTMCase_Sell open_submenu(function() require("src.ui.game3.tm_case").show(BagMenu._session, BagMenu._bag, { @@ -591,7 +592,7 @@ local function begin_sell(row) }) end) return - elseif num == ItemsData.ITEM_BERRY_POUCH then + elseif frlgCases and num == ItemsData.ITEM_BERRY_POUCH then -- src/item_menu.c:1830 GoToBerryPouch_Sell open_submenu(function() require("src.ui.game3.berry_pouch").show(BagMenu._session, BagMenu._bag, { @@ -848,7 +849,8 @@ local function handle_menu_input(input) end else local numId = ItemsData.toNumericId(row.id) - if numId == ItemsData.ITEM_TM_CASE or row.id == "TM_CASE" then + local frlgCases = require("src.core.game3.profile").family(BagMenu._session) ~= "rse" + if frlgCases and (numId == ItemsData.ITEM_TM_CASE or row.id == "TM_CASE") then local savedState = { pocketIdx = BagMenu.pocketIdx, cursor = BagMenu.cursor, scroll = BagMenu.scroll } local TmCase = require("src.ui.game3.tm_case") open_submenu(function() @@ -866,7 +868,7 @@ local function handle_menu_input(input) }) end) return - elseif numId == ItemsData.ITEM_BERRY_POUCH or row.id == "BERRY_POUCH" then + elseif frlgCases and (numId == ItemsData.ITEM_BERRY_POUCH or row.id == "BERRY_POUCH") then local savedState = { pocketIdx = BagMenu.pocketIdx, cursor = BagMenu.cursor, scroll = BagMenu.scroll } local BerryPouch = require("src.ui.game3.berry_pouch") open_submenu(function() diff --git a/src/ui/game3/coins_box.lua b/src/ui/game3/coins_box.lua index 7a3f783b..467c1d3d 100644 --- a/src/ui/game3/coins_box.lua +++ b/src/ui/game3/coins_box.lua @@ -70,6 +70,14 @@ end function CoinsBox.draw() if not CoinsBox.visible then return end + if require("src.core.game3.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) + local text = RomText.plain("gText_Coins", { stringVars = { tostring(clamp(CoinsBox._amount)) } }) + Window.printPx(text, CoinsBox.x * 8 + 64 - FrlgFont.measure(text), CoinsBox.y * 8 + 1) + return + end -- pokefirered/src/coins.c:79 local left = CoinsBox.x + 1 local top = CoinsBox.y + 1 diff --git a/src/ui/game3/egg_hatch.lua b/src/ui/game3/egg_hatch.lua index cac678dd..e42972be 100644 --- a/src/ui/game3/egg_hatch.lua +++ b/src/ui/game3/egg_hatch.lua @@ -374,11 +374,13 @@ function EggHatch.update(dt) -- pokefirered/src/daycare.c:1935 IsFanfareTaskInactive local quiet = (not Audio.isFanfareFinished) or Audio.isFanfareFinished() if quiet and EggHatch._timer > 1 then + -- pokefirered/src/daycare.c:1944 gText_NickHatchPrompt, pokeemerald/src/egg_hatch.c:673 + local key = require("src.core.game3.profile").family(EggHatch._session) == "rse" + and "gText_NicknameHatchPrompt" or "gText_NickHatchPrompt" + local text = RomText.box(key, { stringVars = { EggHatch._name } }) EggHatch._state = "nickname_msg" EggHatch._timer = 0 - -- pokefirered/src/daycare.c:1944 gText_NickHatchPrompt - Message.show(RomText.box("gText_NickHatchPrompt", { stringVars = { EggHatch._name } }), - { stay = true, frame = "battle" }) + Message.show(text, { stay = true, frame = "battle" }) end elseif st == "nickname_msg" then diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index fb66bb72..cf6d6881 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -1258,7 +1258,12 @@ function PartyMenu.enterChosenMon(slot) se("SE_FAILURE") -- pokefirered/src/party_menu.c:3769 local key = (max == 2) and "gText_NoMoreThanTwoMayEnter" or "gText_NoMoreThanThreeMayEnter" - show_rom_message(key, nil, function() PartyMenu.mode = "choose_multi" end) + local vars = nil + if isRse() then + -- pokeemerald/src/party_menu.c:3544 + key, vars = "gText_NoMoreThanVar1Pkmn", { tostring(max) } + end + show_rom_message(key, vars, function() PartyMenu.mode = "choose_multi" end) return false end se("SE_SELECT") @@ -1798,13 +1803,19 @@ function PartyMenu.handleInput(input) if res.action == "dig" then -- pokefirered/src/party_menu.c:3946 destMap = session.escapeWarp and session.escapeWarp.map - key = "gText_EscapeFromHereAndReturnTo" + key = isRse() and "gText_EscapeFromHere" or "gText_EscapeFromHereAndReturnTo" end local game = Field._game local def = game and game.data and game.data.maps and game.data.maps[destMap] - local Sections = require("src.import.gba.map_sections_extract") - -- pokefirered/src/region_map.c:3828 GetMapNameGeneric - local placeName = Sections.getPlaceName(destMap, def and def.regionMapSectionId) + local placeName + if isRse() then + -- pokeemerald/src/party_menu.c:3741 + placeName = require("src.ui.game3.rse.mapsec").name(def and def.regionMapSectionId) + else + local Sections = require("src.import.gba.map_sections_extract") + -- pokefirered/src/region_map.c:3828 GetMapNameGeneric + placeName = Sections.getPlaceName(destMap, def and def.regionMapSectionId) + end -- pokefirered/src/party_menu.c:3984 DisplayFieldMoveExitAreaMessage PartyMenu.showYesNo(RomText.box(key, { stringVars = { placeName }, maxWidth = 216 }), function(yes) if yes then diff --git a/src/ui/game3/rse/bag_menu.lua b/src/ui/game3/rse/bag_menu.lua index bf6341df..3bd04575 100644 --- a/src/ui/game3/rse/bag_menu.lua +++ b/src/ui/game3/rse/bag_menu.lua @@ -505,7 +505,9 @@ function RseBag.draw(Bag) drawDescription(RomText.plain("gText_Var1IsSelected", { stringVars = { sel.name } }), pal) drawGrid(Bag, sel) elseif (Bag.mode == "toss" or Bag.mode == "deposit") and sel then - drawDescription(RomText.plain("gText_TossHowManyVar1s", { stringVars = { sel.name } }), pal) + -- pokeemerald/src/item_menu.c:2203 + local key = Bag.mode == "deposit" and "gText_DepositHowManyVar1" or "gText_TossHowManyVar1s" + drawDescription(RomText.plain(key, { stringVars = { sel.name } }), pal) drawQuantity(pocket, Bag.tossQty or 1) elseif Bag.mode == "toss_confirm" and sel then drawDescription(RomText.plain("gText_ConfirmTossItems", { stringVars = { sel.name, tostring(Bag.tossQty) } }), pal) diff --git a/src/ui/game3/rse/move_relearner.lua b/src/ui/game3/rse/move_relearner.lua new file mode 100644 index 00000000..9e35971b --- /dev/null +++ b/src/ui/game3/rse/move_relearner.lua @@ -0,0 +1,453 @@ +-- pokeemerald/src/move_relearner.c:475 DoMoveRelearnerMain + +local Stack = require("src.ui.game3.stack") +local Window = require("src.ui.game3.window") +local FrlgFont = require("src.ui.game3.frlg_font") +local Pokemon = require("src.core.game3.pokemon") +local MoveLearn = require("src.core.game3.move_learn") +local LearnMove = require("src.core.game3.battle.learn_move") +local SummaryData = require("src.core.game3.summary_data") +local RomText = require("src.core.game3.rom_text") +local Kit = require("src.ui.game3.rse.scene_kit") + +local RseRelearner = {} + +RseRelearner.SUB = "rse/move_relearner" +RseRelearner.open = false +RseRelearner.state = "list" +RseRelearner.cursor = 1 +RseRelearner.scroll = 0 +RseRelearner.yesNoCursor = 1 +RseRelearner.contest = false +RseRelearner.t = 0 + +-- pokeemerald/src/menu_specialized.c:108 sMoveRelearnerWindowTemplates +local WIN_DESC = Window.template(1, 1, 16, 12) +local WIN_LIST = Window.template(19, 1, 10, 12) +local WIN_MSG = Window.template(4, 15, 22, 4) +-- pokeemerald/src/menu_specialized.c:159 sMoveRelearnerYesNoMenuTemplate +local WIN_YESNO = Window.template(22, 8, 5, 4) +RseRelearner.WIN = { desc = WIN_DESC, list = WIN_LIST, msg = WIN_MSG, yesno = WIN_YESNO } + +-- pokeemerald/src/menu_specialized.c:747 +local MAX_SHOWN = 6 +local NARROW = { font = "narrow" } + +-- pokeemerald/src/move_relearner.c:254 +local MODE_ARROWS = { { "left", 27, 16 }, { "right", 117, 16 } } +-- pokeemerald/src/move_relearner.c:269 +local LIST_UP, LIST_DOWN = { 192, 8 }, { 192, 104 } + +local function se(name) + local SE = require("src.core.game3.se_ids") + pcall(function() require("src.core.game3.audio").playSe(SE[name]) end) +end + +function RseRelearner.isOpen() + return RseRelearner.open +end + +function RseRelearner.moves() + return RseRelearner._moves or {} +end + +local function total_rows() + return #RseRelearner.moves() + 1 +end + +local function shown() + return math.min(MAX_SHOWN, total_rows()) +end + +local function clamp_cursor() + local total = total_rows() + if RseRelearner.cursor > total then RseRelearner.cursor = total end + if RseRelearner.cursor < 1 then RseRelearner.cursor = 1 end + local n = shown() + if RseRelearner.cursor <= RseRelearner.scroll then RseRelearner.scroll = RseRelearner.cursor - 1 end + if RseRelearner.cursor > RseRelearner.scroll + n then RseRelearner.scroll = RseRelearner.cursor - n end + if RseRelearner.scroll > total - n then RseRelearner.scroll = total - n end + if RseRelearner.scroll < 0 then RseRelearner.scroll = 0 end +end + +local function mon_name() + return Pokemon.displayMonName(RseRelearner._mon) +end + +local function selected_move() + return RseRelearner.moves()[RseRelearner.cursor] +end + +local function clean(text) + text = tostring(text or ""):gsub("\\n", "\n") + text = text:gsub("\\p$", ""):gsub("\\p", "\n") + return text +end + +-- pokeemerald/src/move_relearner.c:836 ShowTeachMoveText +local function to_list() + RseRelearner.state = "list" + RseRelearner.prompt = RomText.plain("gText_TeachWhichMoveToPkmn", { stringVars = { mon_name() } }) +end + +local function push_message(text, cb) + RseRelearner.state = "message" + RseRelearner.prompt = clean(text) + RseRelearner._messageCb = cb +end + +local function ask_yes_no(text, cb) + RseRelearner.state = "yesno" + RseRelearner.prompt = clean(text) + RseRelearner.yesNoCursor = 1 + RseRelearner._yesNoCb = cb +end + +local function party_slot() + local party = RseRelearner._session and RseRelearner._session.party + if type(party) == "table" then + for i = 1, #party do + if party[i] == RseRelearner._mon then return party, i end + end + end + return { RseRelearner._mon }, 1 +end + +-- pokeemerald/src/move_relearner.c:661 ShowSelectMovePokemonSummaryScreen +local function open_forget_screen(_labels, cb) + local okS, SummaryMenu = pcall(require, "src.ui.game3.summary_menu") + if not (okS and SummaryMenu and SummaryMenu.openMenu) then + if cb then cb(nil) end + return + end + local party, slot = party_slot() + local okOpen = pcall(SummaryMenu.openMenu, party, slot, { + session = RseRelearner._session, + mode = "select_move", + moveToLearn = RseRelearner._pendingMoveId, + onSelectMove = function(slotIdx) + if cb then cb(slotIdx) end + end, + }) + if not okOpen and cb then cb(nil) end +end + +function RseRelearner.finish(learned) + if not RseRelearner.open then return end + RseRelearner.open = false + RseRelearner.state = "list" + RseRelearner._messageCb = nil + RseRelearner._yesNoCb = nil + Stack.pop("move_relearner") + local cb = RseRelearner._onDone + RseRelearner._onDone = nil + if cb then cb(learned == true) end +end + +-- pokeemerald/src/move_relearner.c:517 +local function start_learn(moveId) + RseRelearner._pendingMoveId = moveId + LearnMove.begin({ + mon = RseRelearner._mon, + moveId = moveId, + relearner = true, + displayName = mon_name(), + pushMsg = push_message, + askYesNo = ask_yes_no, + askForget = open_forget_screen, + onDone = function(learned) + if learned then + RseRelearner.finish(true) + else + -- pokeemerald/src/move_relearner.c:637 + RseRelearner._moves = MoveLearn.relearnableMoves(RseRelearner._mon) + clamp_cursor() + to_list() + end + end, + }) +end + +function RseRelearner.show(mon, opts) + opts = opts or {} + RseRelearner.open = true + RseRelearner._mon = mon + RseRelearner._session = opts.session + RseRelearner._onDone = opts.onDone + RseRelearner._moves = MoveLearn.relearnableMoves(mon) + RseRelearner._pendingMoveId = nil + RseRelearner._messageCb = nil + RseRelearner._yesNoCb = nil + RseRelearner.cursor = 1 + RseRelearner.scroll = 0 + -- pokeemerald/src/move_relearner.c:406 + RseRelearner.contest = false + RseRelearner.t = 0 + clamp_cursor() + to_list() + Stack.push("move_relearner", RseRelearner, { hideBelow = true, fullscreen = true }) +end + +-- pokeemerald/src/move_relearner.c:811 +function RseRelearner.giveUpPrompt() + ask_yes_no(RomText.plain("gText_MoveRelearnerGiveUp", { stringVars = { mon_name() } }), function(yes) + if yes then + RseRelearner.finish(false) + else + to_list() + end + end) +end + +-- pokeemerald/src/menu_helpers.c:252 GetLRKeysPressed +local function lr_pressed(input) + if not (input:wasPressed("l") or input:wasPressed("r")) then return false end + local ok, lr = pcall(function() return require("src.core.game3.options").lrMode(RseRelearner._session) end) + return ok and lr == true +end + +function RseRelearner.handleInput(input) + if not RseRelearner.open then return end + RseRelearner.t = RseRelearner.t + 1 + + if RseRelearner.state == "message" then + if input:wasPressed("a") or input:wasPressed("b") then + se("SE_SELECT") + local cb = RseRelearner._messageCb + RseRelearner._messageCb = nil + to_list() + if cb then cb() end + end + return + end + + if RseRelearner.state == "yesno" then + if input:wasPressed("up") and RseRelearner.yesNoCursor ~= 1 then + RseRelearner.yesNoCursor = 1 + se("SE_SELECT") + elseif input:wasPressed("down") and RseRelearner.yesNoCursor ~= 2 then + RseRelearner.yesNoCursor = 2 + se("SE_SELECT") + elseif input:wasPressed("a") or input:wasPressed("b") then + se("SE_SELECT") + local yes = input:wasPressed("a") and RseRelearner.yesNoCursor == 1 + local cb = RseRelearner._yesNoCb + RseRelearner._yesNoCb = nil + to_list() + if cb then cb(yes) end + end + return + end + + local total = total_rows() + if input:wasPressed("up") then + if RseRelearner.cursor > 1 then + RseRelearner.cursor = RseRelearner.cursor - 1 + clamp_cursor() + se("SE_SELECT") + end + elseif input:wasPressed("down") then + if RseRelearner.cursor < total then + RseRelearner.cursor = RseRelearner.cursor + 1 + clamp_cursor() + se("SE_SELECT") + end + elseif input:wasPressed("left") or input:wasPressed("right") or lr_pressed(input) then + -- pokeemerald/src/move_relearner.c:786 + se("SE_SELECT") + RseRelearner.contest = not RseRelearner.contest + elseif input:wasPressed("a") then + se("SE_SELECT") + local moveId = selected_move() + if moveId then + -- pokeemerald/src/move_relearner.c:819 + ask_yes_no(RomText.plain("gText_MoveRelearnerTeachMoveConfirm", + { stringVars = { mon_name(), Pokemon.moveName(moveId) } }), function(yes) + if yes then + start_learn(moveId) + else + to_list() + end + end) + else + RseRelearner.giveUpPrompt() + end + elseif input:wasPressed("b") then + se("SE_SELECT") + RseRelearner.giveUpPrompt() + end +end + +local function manifest() + return Kit.manifest(RseRelearner.SUB) +end + +local function frame(tpl) + Window.stdFrame(tpl) + Window.fill(tpl, 1, 1, 1, 1) +end + +local function type_name(t) + local Types = require("src.core.game3.battle.types") + local id = tonumber(t) + if not id and type(t) == "string" then id = Types.ID[t:upper()] end + local ok, name = pcall(Types.name, id or 0) + return ok and name or "" +end + +local function print_at(text, x, y, opts) + FrlgFont.draw(tostring(text or ""), WIN_DESC.left * 8 + x, WIN_DESC.top * 8 + y, opts) +end + +local function right(text, edge, y) + print_at(text, edge - FrlgFont.measure(text), y) +end + +local function center(text, width, y) + print_at(text, math.floor((width - FrlgFont.measure(text)) / 2), y) +end + +local function print_lines(text, x, y, opts) + local pitch = FrlgFont.linePitch(opts) + for line in (tostring(text or "") .. "\n"):gmatch("(.-)\n") do + print_at(line, x, y, opts) + y = y + pitch + end +end + +-- pokeemerald/src/menu_specialized.c:752 MoveRelearnerLoadBattleMoveDescription +local function draw_battle(moveId) + center(RomText.plain("gText_MoveRelearnerBattleMoves"), 128, 1) + local ppLabel = RomText.plain("gText_MoveRelearnerPP") + print_at(ppLabel, 4, 41) + right(RomText.plain("gText_MoveRelearnerPower"), 106, 25) + right(RomText.plain("gText_MoveRelearnerAccuracy"), 106, 41) + if not moveId then return end + local row = Pokemon.battleMove(moveId) or {} + print_at(type_name(row.type), 4, 25) + print_at(tostring(tonumber(row.pp) or 0), 4 + FrlgFont.measure(ppLabel), 41) + local power, acc = tonumber(row.power) or 0, tonumber(row.accuracy) or 0 + local dashes = RomText.plain("gText_ThreeDashes") + print_at(power < 2 and dashes or tostring(power), 106, 25) + print_at(acc == 0 and dashes or tostring(acc), 106, 41) + print_lines(SummaryData.moveDescription(moveId, Pokemon.moveName(moveId)), 0, 65, NARROW) +end + +local function contest_row(moveId) + local c = Kit.loadLua("data/generated/gba/pokemon/contest_moves.lua") + local cm = c and c.moves and c.moves[moveId] + local eff = cm and c.effects and c.effects[cm.effect] + return cm, eff, c +end + +-- pokeemerald/src/move_relearner.c:919 MoveRelearnerShowHideHearts +local function heart_counts(eff) + local appeal = eff and eff.appeal or 0 + local jam = eff and eff.jam or 0 + appeal = appeal == 0xFF and 0 or math.floor(appeal / 10) + jam = jam == 0xFF and 0 or math.floor(jam / 10) + return appeal, jam +end +RseRelearner.heartCounts = heart_counts + +local quads = {} +local function heart_quad(frame, h, sw, sh) + local q = quads[frame] + if not q then + q = love.graphics.newQuad(0, frame * h.h, h.w, h.h, sw, sh) + quads[frame] = q + end + return q +end + +local function draw_hearts(eff) + local m = manifest() + local h = m and m.hearts + local img = h and Kit.image(h.png) + if not img then return end + local sw, sh = img:getDimensions() + local appeal, jam = heart_counts(eff) + love.graphics.setColor(1, 1, 1, 1) + for i = 0, 7 do + -- pokeemerald/src/move_relearner.c:856 + local x = (i % 4) * 8 + 104 - h.w / 2 + local ya = math.floor(i / 4) * 8 + 36 - h.h / 2 + local yj = math.floor(i / 4) * 8 + 52 - h.h / 2 + local fa = i < appeal and h.names.appealFull or h.names.appealEmpty + local fj = i < jam and h.names.jamFull or h.names.jamEmpty + love.graphics.draw(img, heart_quad(fa, h, sw, sh), x, ya) + love.graphics.draw(img, heart_quad(fj, h, sw, sh), x, yj) + end +end + +-- pokeemerald/src/menu_specialized.c:814 MoveRelearnerMenuLoadContestMoveDescription +local function draw_contest(moveId) + center(RomText.plain("gText_MoveRelearnerContestMovesTitle"), 128, 1) + right(RomText.plain("gText_MoveRelearnerAppeal"), 92, 25) + right(RomText.plain("gText_MoveRelearnerJam"), 92, 41) + if not moveId then return end + local cm, eff, c = contest_row(moveId) + if cm then print_at(c.categories and c.categories[cm.category] or "", 4, 25) end + if eff then print_lines(eff.description, 0, 65, NARROW) end + draw_hearts(eff) +end + +local function draw_list() + local moves = RseRelearner.moves() + local ox, oy = WIN_LIST.left * 8, WIN_LIST.top * 8 + local pitch = Window.optionHeight() + for i = 1, shown() do + local idx = RseRelearner.scroll + i + local y = oy + 1 + (i - 1) * pitch + -- pokeemerald/src/move_relearner.c:913 + local label = moves[idx] and Pokemon.moveName(moves[idx]) or RomText.plain("gText_Cancel") + Window.printPx(label, ox + 8, y) + end + Window.cursorPx(ox, oy + 1 + (RseRelearner.cursor - RseRelearner.scroll - 1) * pitch) +end + +local function draw_arrows() + local BagChrome = require("src.ui.game3.rse.bag_chrome") + local t = RseRelearner.t + for _, a in ipairs(MODE_ARROWS) do BagChrome.drawArrow(a[1], a[2], a[3], t) end + if RseRelearner.scroll > 0 then BagChrome.drawArrow("up", LIST_UP[1], LIST_UP[2], t) end + if RseRelearner.scroll < total_rows() - shown() then BagChrome.drawArrow("down", LIST_DOWN[1], LIST_DOWN[2], t) end +end + +function RseRelearner.draw() + if not RseRelearner.open then return end + if not (love and love.graphics) then return end + -- pokeemerald/src/move_relearner.c:415 + love.graphics.setColor(0, 0, 0, 1) + love.graphics.rectangle("fill", 0, 0, 240, 160) + love.graphics.setColor(1, 1, 1, 1) + -- pokeemerald/src/menu_specialized.c:703 InitMoveRelearnerWindows + frame(WIN_DESC) + frame(WIN_LIST) + frame(WIN_MSG) + + local moveId = selected_move() + if RseRelearner.contest then draw_contest(moveId) else draw_battle(moveId) end + draw_list() + if RseRelearner.state == "list" then draw_arrows() end + + if RseRelearner.prompt then + local y = WIN_MSG.top * 8 + 1 + for line in (RseRelearner.prompt .. "\n"):gmatch("(.-)\n") do + Window.printPx(line, WIN_MSG.left * 8, y) + y = y + FrlgFont.linePitch() + end + end + + if RseRelearner.state == "yesno" then + -- pokeemerald/src/menu_specialized.c:874 MoveRelearnerCreateYesNoMenu + frame(WIN_YESNO) + local ox, oy = WIN_YESNO.left * 8, WIN_YESNO.top * 8 + 1 + local pitch = Window.optionHeight() + Window.printPx(RomText.plain("gText_Yes"), ox + 8, oy) + Window.printPx(RomText.plain("gText_No"), ox + 8, oy + pitch) + Window.cursorPx(ox, oy + (RseRelearner.yesNoCursor - 1) * pitch) + end +end + +return RseRelearner diff --git a/src/ui/game3/rse/shop_menu.lua b/src/ui/game3/rse/shop_menu.lua index 56915021..4a9e5a26 100644 --- a/src/ui/game3/rse/shop_menu.lua +++ b/src/ui/game3/rse/shop_menu.lua @@ -467,6 +467,14 @@ local function draw_money_label() end end +RseShop.drawMoneyBox = function(amount) + local m = WIN.money + Window.stdFrame(m) + Window.fill(m, 1, 1, 1, 1) + draw_money_label() + RseShop.drawMoneyAmount(nil, m, amount) +end + local iconsImage local blankIcons = {} diff --git a/src/ui/game3/rse/summary_menu.lua b/src/ui/game3/rse/summary_menu.lua index 4b0fa4cd..4693649e 100644 --- a/src/ui/game3/rse/summary_menu.lua +++ b/src/ui/game3/rse/summary_menu.lua @@ -465,12 +465,6 @@ local function drawSkills(m, Sm, mon) drawTile(m, "exp", t, (21 + i) * 8, 18 * 8) ticks = math.max(0, ticks - 8) end - -- pokeemerald/src/pokemon_summary_screen.c:1493 - local ailment = SummaryData.statusAilment(mon) - if ailment and ailment > 0 then - put(m, win(m, W.STATUS), "gText_Status", 2, 1, 1) - drawFrame(m.status, ailment - 1, 64 - 16, 152 - 4) - end end -- pokeemerald/src/pokemon_summary_screen.c:3527 @@ -566,6 +560,10 @@ function RseSummary.draw(Sm) love.graphics.setColor(1, 1, 1, 1) local L = m.layers drawLayer(egg and L.info_egg or L.info) + local ailment = not egg and SummaryData.statusAilment(mon) or 0 + if ailment > 0 and m.statusPlate then + drawFrame({ png = m.statusPlate.png, w = 80, h = 16 }, 0, m.statusPlate.x, m.statusPlate.y) + end if page == 1 then drawLayer(L.skills, 80, 240) end if page == 2 then drawLayer(L.battle_moves, detail and 0 or 80, 240) end if page == 3 then drawLayer(L.contest_moves, detail and 0 or 80, 240) end @@ -574,6 +572,11 @@ function RseSummary.draw(Sm) drawPagination(m, page, minPage, maxPage) drawTitles(m, page, detail, Sm) drawPortrait(m, Sm, mon, egg, detail, page) + if ailment > 0 and not detail then + -- pokeemerald/src/pokemon_summary_screen.c:1493 + put(m, win(m, W.STATUS), "gText_Status", 2, 1, 1) + drawFrame(m.status, ailment - 1, 64 - 16, 152 - 4) + end if page == 0 then drawInfo(m, Sm, mon, egg) elseif page == 1 then drawSkills(m, Sm, mon) else drawMoves(m, Sm, mon, page == 3, detail) end diff --git a/src/ui/game3/sell_flow.lua b/src/ui/game3/sell_flow.lua index 489dd113..405afe91 100644 --- a/src/ui/game3/sell_flow.lua +++ b/src/ui/game3/sell_flow.lua @@ -17,6 +17,10 @@ local function se(id) pcall(function() require("src.core.game3.audio").playSe(id) end) end +local function is_rse(session) + return require("src.core.game3.profile").family(session) == "rse" +end + local function price_of(itemId) local info = ItemsData.info(itemId) return math.max(0, math.floor(tonumber(info and info.price) or 0)) @@ -49,22 +53,36 @@ function SellFlow.start(opts) self.colors = dialog_colors() if price_of(opts.itemId) == 0 then self.state = "cant" - self.text = RomText.box("gText_OhNoICantBuyThat", { stringVars = { self.name } }) + if is_rse(self.session) then + self.text = RomText.box("gText_CantBuyKeyItem", { stringVars = { [2] = self.name } }) + else + self.text = RomText.box("gText_OhNoICantBuyThat", { stringVars = { self.name } }) + end self.textColors = self.colors return self end local owned = math.max(1, tonumber(opts.owned) or 1) - self.owned = math.min(99, owned) + self.owned = math.min(self:rseBerry() and 999 or 99, owned) if owned == 1 then self:ask() else self.state = "qty" - self.text = RomText.box("gText_HowManyWouldYouLikeToSell", { stringVars = { self.name } }) + if is_rse(self.session) then + self.text = RomText.box("gText_HowManyToSell", { stringVars = { [2] = self.name } }) + else + self.text = RomText.box("gText_HowManyWouldYouLikeToSell", { stringVars = { self.name } }) + end self.textColors = self.colors end return self end +function SellFlow:rseBerry() + if not is_rse(self.session) then return false end + local ok, BagMenu = pcall(require, "src.ui.game3.bag_menu") + return ok and BagMenu.currentPocket and BagMenu.currentPocket() == "BERRY_POUCH" or false +end + function SellFlow:total() return self.unit * self.qty end @@ -73,16 +91,24 @@ end function SellFlow:ask() self.state = "confirm" self.yesNo = 1 - self.text = RomText.box("gText_ICanPayThisMuch_WouldThatBeOkay", - { stringVars = { [3] = tostring(self:total()) } }) + if is_rse(self.session) then + self.text = RomText.box("gText_ICanPayVar1", { stringVars = { tostring(self:total()) } }) + else + self.text = RomText.box("gText_ICanPayThisMuch_WouldThatBeOkay", + { stringVars = { [3] = tostring(self:total()) } }) + end self.textColors = self.colors end -- src/item_menu.c:1917 Task_SellItem_Yes, :1928 Task_FinalizeSaleToShop function SellFlow:commit() local earn = self:total() - self.text = RomText.box("gText_TurnedOverItemsWorthYen", - { stringVars = { self.name, [3] = tostring(earn) } }) + if is_rse(self.session) then + self.text = RomText.box("gText_TurnedOverVar1ForVar2", { stringVars = { tostring(earn), self.name } }) + else + self.text = RomText.box("gText_TurnedOverItemsWorthYen", + { stringVars = { self.name, [3] = tostring(earn) } }) + end self.textColors = FrlgFont.COLOR.NORMAL self.state = "done" se(SE.SE_SHOP) @@ -184,9 +210,41 @@ local function draw_money_box(amount) Window.printPx(s, 8 + 64 - w, 8 + 12, { small = true }) end +-- pokeemerald/src/item_menu.c:2120 InitSellHowManyInput, :1201 PrintItemSoldAmount +function SellFlow:drawRse() + local st = self.state + local Shop = require("src.ui.game3.rse.shop_menu") + local Chrome = require("src.ui.game3.chrome") + if st ~= "cant" then + Shop.drawMoneyBox(tonumber(self.session and self.session.money) or 0) + end + Window.dialogueFrame() + local w = Chrome.DLG_W * 8 + FrlgFont.draw(FrlgFont.wrap(self.text, w), Chrome.DLG_LEFT * 8, Chrome.DLG_TOP * 8 + 1, + { maxWidth = w, colors = FrlgFont.COLOR.NORMAL }) + if st == "qty" then + local q = Shop.WIN.qty + Window.stdFrame(q) + Window.fill(q, 1, 1, 1, 1) + local digits = self:rseBerry() and 3 or 2 + Window.printPx(RomText.plain("gText_xVar1", { stringVars = { string.format("%0" .. digits .. "d", self.qty) } }), + q.left * 8, q.top * 8 + 1) + Shop.drawMoneyAmount(nil, q, self:total()) + elseif st == "confirm" then + local yn = Shop.WIN.yesno + Window.stdFrame(yn) + Window.fill(yn, 1, 1, 1, 1) + local pitch = Window.optionHeight() + Window.printPx(RomText.plain("gText_Yes"), yn.left * 8 + 8, yn.top * 8 + 1) + Window.printPx(RomText.plain("gText_No"), yn.left * 8 + 8, yn.top * 8 + 1 + pitch) + Window.cursorPx(yn.left * 8, yn.top * 8 + 1 + (self.yesNo - 1) * pitch) + end +end + function SellFlow:draw() local st = self.state if not st then return end + if is_rse(self.session) then return self:drawRse() end local Chrome = require("src.ui.game3.chrome") if st ~= "cant" then draw_money_box(tonumber(self.session and self.session.money) or 0) diff --git a/tests/drivers/battle_move_swap_common.lua b/tests/drivers/battle_move_swap_common.lua new file mode 100644 index 00000000..b76604ef --- /dev/null +++ b/tests/drivers/battle_move_swap_common.lua @@ -0,0 +1,183 @@ +local U = require("tests.drivers.util") + +local M = {} + +function M.run(game, opts) + local DIR = os.getenv("POKEPORT_SHOT_DIR") or opts.dir + 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(opts.newGame) + 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 C = require("src.core.game3.constants").of(opts.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 + local function mv(name) return C.moves.byName["MOVE_" .. name] end + + local function set_party(moves, pp) + session.party = {} + Party.giveMon(session, sp("CHARIZARD"), 50, "CHARIZARD") + local m = session.party[1] + m.moves = moves + m.pp = pp + m.maxPp = { 35, 40, 25, 30 } + m.ppBonusesPacked = 57 + m.hp = m.maxHp or m.hp + return m + end + + local lastTap = 0 + local f = 0 + local function pump_text() + f = f + 1 + if Ui.dialogPending and Ui.dialogPending() and f - lastTap >= 14 then + lastTap = f + U.tap(game, "a") + return true + end + return false + end + local function at_command() + return Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu" + end + + local function open_moves(tag) + local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = sp("ZIGZAGOON"), level = 3 }, {}) + result(ok == true, tag .. " battle started " .. tostring(err or "")) + if not ok then return false end + for _ = 1, 6000 do + if at_command() then break end + if not pump_text() then U.wait(1) end + end + result(at_command(), tag .. " reached command menu") + if not at_command() then return false end + local st = Battle.getState() + st.enemy.mon.maxHp = 999 + st.enemy.mon.hp = 999 + U.wait(30) + U.tap(game, "a") + U.wait(30) + result(Ui._mode == "moves", tag .. " move menu open") + return Ui._mode == "moves" + end + + local function end_battle() + Battle.abort("run") + for _ = 1, 600 do + if not Battle.isActive() then break end + U.wait(1) + end + U.wait(60) + end + + local function same(a, b) + for i = 1, 4 do if (a[i] or 0) ~= (b[i] or 0) then return false end end + return true + end + + local base = { mv("FLAMETHROWER"), mv("GROWL"), mv("SLASH"), mv("EMBER") } + local basePp = { 11, 22, 13, 24 } + + local mon = set_party({ base[1], base[2], base[3], base[4] }, { basePp[1], basePp[2], basePp[3], basePp[4] }) + if not open_moves("four") then finish() return end + local st = Battle.getState() + + st.link = true + U.tap(game, "select") + U.wait(6) + result(Ui._swap == nil, "SELECT ignored in link battle") + st.link = nil + + U.tap(game, "select") + U.wait(6) + result(Ui._swap ~= nil and Ui._swap.cursor == 1, "SELECT opens swap with second cursor on slot 2") + U.wait(10) + U.still(game, DIR .. "/" .. opts.tag .. "_01_swap_start.png") + + U.tap(game, "down") + U.wait(6) + result(Ui._swap and Ui._swap.cursor == 3, "down moves second cursor to slot 4") + U.tap(game, "left") + U.wait(6) + result(Ui._swap and Ui._swap.cursor == 2, "left moves second cursor to slot 3") + U.wait(10) + U.still(game, DIR .. "/" .. opts.tag .. "_02_swap_cursor_slot3.png") + + U.tap(game, "b") + U.wait(6) + result(Ui._swap == nil, "B leaves swap mode") + result(same(st.player.mon.moves, base), "cancel leaves battler order unchanged") + result(same(st.player.mon.pp, basePp), "cancel leaves battler pp unchanged") + result(Ui._mode == "moves" and Ui._moveIndex == 1, "cancel keeps move cursor") + + U.tap(game, "select") + U.wait(6) + U.tap(game, "left") + U.wait(6) + U.tap(game, "select") + U.wait(6) + local bmon = st.player.mon + result(Ui._swap == nil, "SELECT confirms; same slot is a no-op") + result(same(bmon.moves, base), "same-slot confirm leaves order unchanged") + + U.tap(game, "select") + U.wait(6) + U.tap(game, "down") + U.wait(6) + U.tap(game, "left") + U.wait(6) + U.tap(game, "a") + U.wait(10) + local want = { base[3], base[2], base[1], base[4] } + local wantPp = { basePp[3], basePp[2], basePp[1], basePp[4] } + local wantMax = { 25, 40, 35, 30 } + result(Ui._swap == nil, "A confirms swap") + result(same(bmon.moves, want), "battler moves swapped 1<->3") + result(same(bmon.pp, wantPp), "battler pp swapped 1<->3") + result(same(bmon.maxPp, wantMax), "battler maxPp swapped 1<->3") + result(bmon.ppBonusesPacked == 27, "battler pp bonuses swapped 1<->3") + result(Ui._moveIndex == 3, "move cursor lands on the swapped-to slot") + U.wait(10) + U.still(game, DIR .. "/" .. opts.tag .. "_03_after_swap.png") + + U.tap(game, "b") + U.wait(20) + result(Ui._mode == "menu", "B from move menu returns to action menu") + U.tap(game, "a") + U.wait(20) + result(Ui._mode == "moves" and same(st.player.mon.moves, want), "reopened move menu keeps order") + end_battle() + result(same(mon.moves, want) and same(mon.pp, wantPp), "session party order persists after the battle") + result(same(mon.maxPp, wantMax) and mon.ppBonusesPacked == 27, "session party maxPp and bonuses persist after the battle") + + local mon1 = set_party({ mv("TACKLE"), 0, 0, 0 }, { 35, 0, 0, 0 }) + if not open_moves("single") then finish() return end + U.tap(game, "select") + U.wait(6) + result(Ui._swap == nil, "SELECT ignored with one move") + result(mon1.moves[1] == mv("TACKLE"), "single move untouched") + end_battle() + + finish() +end + +return M diff --git a/tests/drivers/em_bag_items_flags.lua b/tests/drivers/em_bag_items_flags.lua new file mode 100644 index 00000000..16d8e574 --- /dev/null +++ b/tests/drivers/em_bag_items_flags.lua @@ -0,0 +1,109 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_bag_items_flags" + +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_bag_items_flags failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Bag = require("src.core.game3.bag") + local ItemUse = require("src.core.game3.item_use") + local FieldMoves = require("src.core.game3.field_moves") + local Field = require("src.core.game3.field") + local Flags = require("src.core.game3.scripting.flags") + local Space = require("src.core.game3.scripting.space") + local PartyMenu = require("src.ui.game3.party_menu") + local RomText = require("src.core.game3.rom_text") + local Party = require("src.core.game3.party") + local C = require("src.core.game3.constants").of("emerald") + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return end + session.bag = session.bag or Bag.new() + session.party = {} + Party.giveMonToPlayer(session, C:require("species", "SPECIES_TORCHIC"), 10) + + for _, name in ipairs({ "ITEM_POKE_FLUTE", "ITEM_TM_CASE", "ITEM_BERRY_POUCH" }) do + local ok, kind, text = ItemUse.useField(session, session.bag, C:require("items", name)) + check(ok == false and text and text:find("advice", 1, true) ~= nil, + name .. " is not usable, Dad's advice: " .. tostring(text)) + end + + local up = C:require("flags", "FLAG_SYS_ENC_UP_ITEM") + local down = C:require("flags", "FLAG_SYS_ENC_DOWN_ITEM") + check(FieldMoves.SYS_FLAGS.WHITE_FLUTE_ACTIVE == up and up == 2221, "white flute flag is FLAG_SYS_ENC_UP_ITEM") + check(FieldMoves.SYS_FLAGS.BLACK_FLUTE_ACTIVE == down and down == 2222, "black flute flag is FLAG_SYS_ENC_DOWN_ITEM") + local ok, kind, text = ItemUse.useBlackWhiteFlute(session, C:require("items", "ITEM_WHITE_FLUTE")) + check(ok and kind == "black_white_flute", "white flute used") + check(Flags.getFlag(Space.store, nil, up) == true and Flags.getFlag(Space.store, nil, down) ~= true, "ENC_UP set, ENC_DOWN clear") + check(Flags.getFlag(Space.store, nil, 0x803) ~= true, "trainer flag 0x803 untouched by the flute") + local t = 0x803 + Flags.setFlag(Space.store, nil, t, true) + Field.clearTempFieldEventData(Field._game, session.map) + check(Flags.getFlag(Space.store, nil, up) ~= true, "ENC_UP cleared on map change") + check(Flags.getFlag(Space.store, nil, t) == true, "trainer flag survives map change") + local list = FieldMoves.tempSysFlags() + local names = {} + for _, id in ipairs(list) do names[#names + 1] = Flags.nameFor(id) end + print("[driver] temp flags: " .. table.concat(names, ",")) + check(#list == 5, "five Emerald temp flags") + + local Itemfinder = require("src.core.game3.itemfinder") + check(Itemfinder.textKey("nothing", session) == "gText_ItemFinderNothing", "itemfinder key") + + PartyMenu._chooseOrder = { 1, 2 } + PartyMenu._chooseMax = 2 + local shown + local orig = PartyMenu.showMessage + PartyMenu.showMessage = function(text) shown = text end + PartyMenu.enterChosenMon(3) + PartyMenu.showMessage = orig + check(shown and shown:find("No more than 2", 1, true) ~= nil, "multi party entry text: " .. tostring(shown)) + + check(select(1, pcall(RomText.box, "gText_NicknameHatchPrompt", { stringVars = { "TORCHIC" } })), "hatch prompt key exists") + local EggHatch = require("src.ui.game3.egg_hatch") + check(EggHatch ~= nil, "egg hatch module loads") + + local LB = require("src.core.game3.link.battle") + local FieldRse = require("src.core.game3.scripting.natives_field_rse") + local Rse = require("src.core.game3.rse.init") + Rse.setVar("VAR_LILYCOVE_FAN_CLUB_STATE", 2) + local before = FieldRse.numFans() + check(pcall(FieldRse.updateTrainerFansAfterLinkBattle, true), "fan club update after a link win runs") + check(FieldRse.numFans() >= before, "a link win does not lose fans") + check(Flags.ensurePalletOakHidden(Space.store) == nil and Flags.getFlag(Space.store, nil, 0x2C) ~= true, + "Pallet Oak hide flag not set on Emerald") + local Compat = require("src.mods.Gen3Compat") + Flags.setFlag(Space.store, nil, up, true) + check(Compat.getFlag("FLAG_SYS_ENC_UP_ITEM") == true, "Gen3Compat resolves Emerald flag names") +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/drivers/em_battle_move_swap.lua b/tests/drivers/em_battle_move_swap.lua new file mode 100644 index 00000000..bbdc54c3 --- /dev/null +++ b/tests/drivers/em_battle_move_swap.lua @@ -0,0 +1,10 @@ +local Common = require("tests.drivers.battle_move_swap_common") + +return function(game) + Common.run(game, { + version = "emerald", + tag = "em", + dir = "/tmp/em_battle_move_swap", + newGame = { action = "new_game", name = "NICK", gender = 0 }, + }) +end diff --git a/tests/drivers/em_evolve_ability.lua b/tests/drivers/em_evolve_ability.lua new file mode 100644 index 00000000..6b760ead --- /dev/null +++ b/tests/drivers/em_evolve_ability.lua @@ -0,0 +1,39 @@ +local U = require("tests.drivers.util") + +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_evolve_ability 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 + game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) + U.wait(30) + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local Evolution = require("src.core.game3.evolution") + local Pokemon = require("src.core.game3.pokemon") + local C = require("src.core.game3.constants").of("emerald") + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return finish() end + local TRUANT, VITAL_SPIRIT = 54, 72 + Party.giveMonToPlayer(session, C:require("species", "SPECIES_SLAKOTH"), 18) + local mon = session.party[1] + check(Pokemon.abilityName(mon.ability) == "TRUANT", "Slakoth has Truant (" .. Pokemon.abilityName(mon.ability) .. ")") + Evolution.apply(mon, C:require("species", "SPECIES_VIGOROTH"), session) + check(mon.ability == VITAL_SPIRIT and mon.abilityId == VITAL_SPIRIT, "Vigoroth has Vital Spirit (" .. tostring(mon.ability) .. ")") + Evolution.apply(mon, C:require("species", "SPECIES_SLAKING"), session) + check(mon.ability == TRUANT, "Slaking has Truant (" .. tostring(mon.ability) .. ")") + return finish() +end diff --git a/tests/drivers/em_itemfinder.lua b/tests/drivers/em_itemfinder.lua new file mode 100644 index 00000000..f1002dbc --- /dev/null +++ b/tests/drivers/em_itemfinder.lua @@ -0,0 +1,70 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_itemfinder" + +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_itemfinder failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Field = require("src.core.game3.field") + local Itemfinder = require("src.core.game3.itemfinder") + local RomText = require("src.core.game3.rom_text") + local Message = require("src.ui.game3.message") + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return end + + check(Itemfinder.textKey("nothing", session) == "gText_ItemFinderNothing", "nothing key") + check(Itemfinder.textKey("nearby", session) == "gText_ItemFinderNearby", "nearby key") + check(Itemfinder.textKey("onTop", session) == "gText_ItemFinderOnTop", "on top key") + for _, k in ipairs({ "gText_ItemFinderNothing", "gText_ItemFinderNearby", "gText_ItemFinderOnTop" }) do + check(select(1, pcall(RomText.box, k)), "text exists " .. k) + end + + local ok, kind, text = Field.useItemfinder(session, true) + check(ok == false and kind == "itemfinder", "itemfinder with nothing nearby returns no response") + check(text and text:find("no response", 1, true) ~= nil, "nothing text: " .. tostring(text)) + U.wait(40) + check(Message.isOpen and Message.isOpen(), "message shown") + U.still(game, DIR .. "/nothing.png") + Message.close() + Field.unlock() + + local sawUnderfoot = false + local ok2 = pcall(function() + local r = Field.digUpUnderfootItem(Field._game, { flag = 1 }) + sawUnderfoot = (r == false) + end) + check(ok2 and sawUnderfoot, "underfoot branch only closes the message on Emerald") + Field.unlock() +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/drivers/em_link_trade_menu.lua b/tests/drivers/em_link_trade_menu.lua new file mode 100644 index 00000000..9700ba04 --- /dev/null +++ b/tests/drivers/em_link_trade_menu.lua @@ -0,0 +1,57 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_link_trade_menu" + +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_link_trade_menu failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local C = require("src.core.game3.constants").of("emerald") + local LinkTradeMenu = require("src.ui.game3.link_trade_menu") + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return end + session.party = {} + Party.giveMonToPlayer(session, C:require("species", "SPECIES_TORCHIC"), 10) + Party.giveMonToPlayer(session, C:require("species", "SPECIES_MUDKIP"), 10) + + check(pcall(function() LinkTradeMenu.show() end), "link trade menu opens without throwing") + check(LinkTradeMenu.open == true, "link trade menu is open") + check(LinkTradeMenu.message and LinkTradeMenu.message:find("standby", 1, true) ~= nil, "standby message: " .. tostring(LinkTradeMenu.message)) + U.wait(20) + check(pcall(function() LinkTradeMenu.draw() end), "link trade menu draws") + U.still(game, DIR .. "/trade_menu.png") + LinkTradeMenu.close() + 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 + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/drivers/em_move_relearner.lua b/tests/drivers/em_move_relearner.lua new file mode 100644 index 00000000..c60d27c6 --- /dev/null +++ b/tests/drivers/em_move_relearner.lua @@ -0,0 +1,146 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_move_relearner" + +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_move_relearner failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local Pokemon = require("src.core.game3.pokemon") + local MoveLearn = require("src.core.game3.move_learn") + local Stack = require("src.ui.game3.stack") + local SummaryMenu = require("src.ui.game3.summary_menu") + local MoveTeach = require("src.core.game3.scripting.natives_moveteach") + local Rse = require("src.ui.game3.rse.move_relearner") + local C = require("src.core.game3.constants").of("emerald") + + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return end + + session.party = {} + Party.giveMonToPlayer(session, C:require("species", "SPECIES_TORCHIC"), 40) + local mon = session.party[1] + local start = {} + for i = 1, 4 do start[i] = Pokemon.moveIdAt(mon, i) end + print("[driver] start moves: " .. table.concat({ Pokemon.moveName(start[1]), Pokemon.moveName(start[2]), + Pokemon.moveName(start[3]), Pokemon.moveName(start[4]) }, ", ")) + local offers = MoveLearn.relearnableMoves(mon) + check(#offers >= 6, "relearnable list scrolls, count=" .. #offers) + + local ctx = { specialVars = { [0x8004] = 0 } } + local yielded = MoveTeach.BY_NAME.TeachMoveRelearnerMove(ctx, {}) + check(yielded == true, "TeachMoveRelearnerMove yields to the host") + U.wait(10) + local top = Stack.top() + check(top and top.id == "move_relearner" and top.mod == Rse, "native opened the Emerald relearner screen") + check(Rse.isOpen() and not require("src.ui.game3.move_relearner").isOpen(), "FRLG screen stayed closed") + check(Rse.prompt and Rse.prompt:find("Teach which move", 1, true) ~= nil, "prompt: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/01_list_battle.png") + + for _ = 1, 6 do U.tap(game, "down"); U.wait(6) end + check(Rse.cursor == 7 and Rse.scroll == 1, "cursor on CANCEL, list scrolled (cursor=" .. Rse.cursor .. " scroll=" .. Rse.scroll .. ")") + U.still(game, DIR .. "/02_list_scrolled_cancel.png") + + local want = C:require("moves", "MOVE_EMBER") + local row + for i, id in ipairs(Rse.moves()) do if id == want then row = i end end + if not check(row ~= nil, "EMBER is relearnable") then return end + for _ = 1, 10 do + if Rse.cursor == row then break end + U.tap(game, "up"); U.wait(6) + end + check(Rse.cursor == row, "cursor on EMBER") + U.tap(game, "right"); U.wait(8) + check(Rse.contest == true, "RIGHT switched to CONTEST MOVES") + U.still(game, DIR .. "/03_contest_ember.png") + U.tap(game, "left"); U.wait(8) + check(Rse.contest == false, "LEFT switched back to BATTLE MOVES") + U.still(game, DIR .. "/04_battle_ember.png") + + U.tap(game, "a"); U.wait(10) + check(Rse.state == "yesno" and Rse.prompt:find("Teach", 1, true) ~= nil, "teach confirm: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/05_teach_confirm.png") + U.tap(game, "a"); U.wait(10) + check(Rse.state == "message" and Rse.prompt:find("trying to learn", 1, true) ~= nil, "trying-to-learn page 1: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/06_trying_to_learn.png") + U.tap(game, "a"); U.wait(10) + check(Rse.state == "message" and Rse.prompt:find("four moves", 1, true) ~= nil, "page 2: " .. tostring(Rse.prompt)) + U.tap(game, "a"); U.wait(10) + check(Rse.state == "yesno" and Rse.prompt:find("Delete an older move", 1, true) ~= nil, "delete prompt: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/07_delete_prompt.png") + U.tap(game, "a"); U.wait(10) + check(Rse.state == "message" and Rse.prompt:find("forgotten", 1, true) ~= nil and not Rse.prompt:find("\\", 1, true), + "which-move prompt: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/08_which_move.png") + U.tap(game, "a") + for _ = 1, 120 do + if SummaryMenu.isOpen() then break end + U.wait(2) + end + if not check(SummaryMenu.isOpen() and SummaryMenu._mode == "select_move", "summary opened in select_move") then return end + U.wait(60) + U.still(game, DIR .. "/09_summary_select.png") + U.tap(game, "a") + for _ = 1, 120 do + if not SummaryMenu.isOpen() then break end + U.wait(2) + end + U.wait(10) + check(Rse.state == "message" and Rse.prompt:find("Poof", 1, true) ~= nil, "poof: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/10_poof.png") + local pages = {} + for _ = 1, 8 do + if not Rse.isOpen() then break end + pages[#pages + 1] = Rse.prompt + if #pages == 4 then U.still(game, DIR .. "/11_learned.png") end + U.tap(game, "a"); U.wait(10) + end + print("[driver] pages: " .. table.concat(pages, " | "):gsub("\n", " ")) + check(pages[2] and pages[2]:find("forgot", 1, true) ~= nil, "forgot page") + check(pages[4] and pages[4]:find("learned", 1, true) ~= nil, "learned page") + check(not Rse.isOpen(), "relearner closed") + check(ctx.specialVars[0x8004] == 1, "VAR_0x8004 = 1 after teach, got " .. tostring(ctx.specialVars[0x8004])) + check(Pokemon.knowsMove(mon, want), "mon knows EMBER") + check(not Pokemon.knowsMove(mon, start[1]), "mon forgot " .. tostring(Pokemon.moveName(start[1]))) + + ctx.specialVars[0x8004] = 0 + MoveTeach.BY_NAME.TeachMoveRelearnerMove(ctx, {}) + U.wait(10) + U.tap(game, "b"); U.wait(10) + check(Rse.state == "yesno" and Rse.prompt:find("Give up", 1, true) ~= nil, "give-up prompt: " .. tostring(Rse.prompt)) + U.still(game, DIR .. "/12_give_up.png") + U.tap(game, "a"); U.wait(10) + check(not Rse.isOpen() and ctx.specialVars[0x8004] == 0, "give up closes with VAR_0x8004 = 0") +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/drivers/em_shop_sell.lua b/tests/drivers/em_shop_sell.lua new file mode 100644 index 00000000..1a54908e --- /dev/null +++ b/tests/drivers/em_shop_sell.lua @@ -0,0 +1,113 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_shop_sell" + +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_shop_sell failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Bag = require("src.core.game3.bag") + local BagMenu = require("src.ui.game3.bag_menu") + local ItemsData = require("src.core.game3.items_data") + local C = require("src.core.game3.constants").of("emerald") + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return end + + session.bag = session.bag or Bag.new() + local POTION = C:require("items", "ITEM_POTION") + local BAG_PACK = C:require("items", "ITEM_POKE_BALL") + Bag.add(session.bag, POTION, 3) + session.money = 1000 + local Q = require("src.core.game3.quest_log_recorder") + check(select(1, pcall(Q.event, session, "SoldItemsIncludingItem", { D0 = "X", D1 = "Y", D2 = 1 })), "quest log event is a no-op on Emerald") + + BagMenu.show(session.bag, { session = session, location = "shop" }) + U.wait(30) + local row + for i, r in ipairs(BagMenu.list()) do + if tonumber(r.id) == POTION then row = i break end + end + check(row ~= nil, "potion in the bag") + if not row then return end + for _ = 1, 20 do + if BagMenu.cursor == row then break end + U.tap(game, "down") + U.wait(6) + end + U.tap(game, "a") + U.wait(20) + check(BagMenu.mode == "sell" and BagMenu._sell ~= nil, "sell flow started") + local flow = BagMenu._sell + if not flow then return end + check(flow.state == "qty", "quantity prompt for a stack of 3") + check(flow.text:find("How many", 1, true) ~= nil, "how many text: " .. tostring(flow.text)) + U.wait(10) + U.still(game, DIR .. "/qty.png") + U.tap(game, "up") + U.wait(6) + check(flow.qty == 2, "quantity went to 2") + U.tap(game, "a") + U.wait(10) + check(flow.state == "confirm", "confirm prompt") + check(flow.text:find("I can pay", 1, true) ~= nil and flow.text:find(tostring(flow.unit * 2), 1, true) ~= nil, + "price text: " .. tostring(flow.text)) + U.still(game, DIR .. "/confirm.png") + local before = session.money + U.tap(game, "a") + U.wait(10) + check(flow.state == "done" or flow.state == nil, "sale committed") + check(session.money == before + flow.unit * 2, "money increased by " .. tostring(flow.unit * 2)) + check(Bag.get(session.bag, POTION) == 1, "two potions removed") + check(flow.text:find(ItemsData.displayName(POTION), 1, true) ~= nil, "turned over text names the item: " .. tostring(flow.text)) + U.still(game, DIR .. "/done.png") + U.tap(game, "a") + U.wait(20) + check(BagMenu.mode == "list", "back to the bag list") + + local SellFlow = require("src.ui.game3.sell_flow") + local cant = SellFlow.start({ itemId = C:require("items", "ITEM_POKE_FLUTE"), session = session, bag = session.bag, owned = 1 }) + check(cant.state == "cant" and cant.text:find("can't buy", 1, true) ~= nil, "key item refusal text: " .. tostring(cant.text)) + check(pcall(function() cant:draw() end), "cant state draws") + + local CoinsBox = require("src.ui.game3.coins_box") + CoinsBox.show(1, 1, 1234) + check(pcall(function() CoinsBox.draw() end), "coins box draws on Emerald") + U.wait(2) + U.still(game, DIR .. "/coins.png") + CoinsBox.hide() + for _ = 1, 6 do + U.tap(game, "b") + U.wait(15) + end +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/drivers/em_summary_info_page.lua b/tests/drivers/em_summary_info_page.lua new file mode 100644 index 00000000..c587af28 --- /dev/null +++ b/tests/drivers/em_summary_info_page.lua @@ -0,0 +1,67 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_summary_info_page" + +local failures = 0 +local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end +end + +local function finish() + print((failures == 0 and "PASS" or "FAIL") .. " em_summary_info_page failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local SummaryMenu = require("src.ui.game3.summary_menu") + local C = require("src.core.game3.constants").of("emerald") + local session = Runtime.getSession() + session.party = {} + Party.giveMonToPlayer(session, C:require("species", "SPECIES_TORCHIC"), 9) + Party.giveMonToPlayer(session, C:require("species", "SPECIES_TORCHIC"), 12) + session.party[2].status = "PSN" + U.wait(60) + + local function settle() + for _ = 1, 120 do + if not (SummaryMenu._slide and SummaryMenu._slide.active) then break end + U.wait(1) + end + U.wait(8) + end + + for mi = 1, 2 do + SummaryMenu.openMenu(session.party, mi, {}) + settle() + U.still(game, DIR .. "/m" .. mi .. "_p0_info.png") + U.tap(game, "right") settle() + U.still(game, DIR .. "/m" .. mi .. "_p1_skills.png") + U.tap(game, "right") settle() + U.still(game, DIR .. "/m" .. mi .. "_p2_moves.png") + U.tap(game, "right") settle() + U.still(game, DIR .. "/m" .. mi .. "_p3_contest.png") + U.tap(game, "a") settle() + U.still(game, DIR .. "/m" .. mi .. "_p3_detail.png") + U.tap(game, "b") settle() + U.tap(game, "left") settle() + U.tap(game, "a") settle() + U.still(game, DIR .. "/m" .. mi .. "_p2_detail.png") + for _ = 1, 4 do U.tap(game, "b") settle() end + check(true, "mon " .. mi .. " pages shot") + end +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) + U.wait(30) + local ok, err = xpcall(function() body(game) end, debug.traceback) + if not ok then print("[driver] error " .. tostring(err)) failures = failures + 1 end + return finish() +end diff --git a/tests/drivers/em_teleport_vitamin.lua b/tests/drivers/em_teleport_vitamin.lua new file mode 100644 index 00000000..c670ff1b --- /dev/null +++ b/tests/drivers/em_teleport_vitamin.lua @@ -0,0 +1,178 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_teleport_vitamin" + +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_teleport_vitamin failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Player = require("src.core.game3.player") + local Party = require("src.core.game3.party") + local Bag = require("src.core.game3.bag") + local Pokemon = require("src.core.game3.pokemon") + local PartyMenu = require("src.ui.game3.party_menu") + local StartMenu = require("src.ui.game3.start_menu") + local BagMenu = require("src.ui.game3.bag_menu") + local Field = require("src.core.game3.field") + local FieldEffects = require("src.core.game3.field_effects") + local C = require("src.core.game3.constants").of("emerald") + + local session = Runtime.getSession() + if not check(session and session.version == "emerald", "emerald session") then return end + + local Flags = require("src.core.game3.scripting.flags") + local Space = require("src.core.game3.scripting.space") + Flags.setFlag(Space.store, nil, C:require("flags", "FLAG_SYS_POKEMON_GET"), true) + print("[driver] fresh healMap=" .. tostring(session.healMap) .. " map=" .. tostring(session.map)) + + session.party = {} + Party.giveMonToPlayer(session, C:require("species", "SPECIES_TORCHIC"), 30) + local mon = session.party[1] + mon.moves[1] = C:require("moves", "MOVE_TELEPORT") + session.bag = session.bag or Bag.new() + local ITEM_HP_UP = C:require("items", "ITEM_HP_UP") + Bag.add(session.bag, ITEM_HP_UP, 3) + + Map.load(nil, game, "EM_ROUTE101", { x = 10, y = 12, facing = "down" }) + session.x, session.y, session.facing = 10, 12, "down" + Player.cellX, Player.cellY = 10, 12 + Player.px, Player.py = 160, 192 + Player.targetX, Player.targetY = 10, 12 + Player.facing = "down" + U.wait(90) + local startMap = session.map + + local function openStart(entryId) + for _ = 1, 40 do + if StartMenu.isOpen and StartMenu.isOpen() then break end + U.tap(game, "start") + U.wait(8) + end + for _ = 1, 20 do + local e = StartMenu.ENTRIES and StartMenu.ENTRIES[StartMenu.cursor] + if e and e.id == entryId then break end + U.tap(game, "down") + U.wait(6) + end + U.tap(game, "a") + U.wait(40) + end + + local function teleportStage() + U.tap(game, "start") + U.wait(30) + local ids = {} + for _, e in ipairs(StartMenu.ENTRIES or {}) do ids[#ids + 1] = tostring(e.id) end + print("[driver] start entries: " .. table.concat(ids, ",")) + U.tap(game, "b") + U.wait(20) + openStart("pokemon") + check(PartyMenu.isOpen and PartyMenu.isOpen(), "party menu opens") + U.tap(game, "a") + U.wait(12) + local target + for i, name in ipairs(PartyMenu.ACTIONS or {}) do + if name == "TELEPORT" then target = i end + end + check(target ~= nil, "TELEPORT is on the action list") + if not target then return end + for _ = 1, 12 do + if PartyMenu.actionCursor == target then break end + U.tap(game, "down") + U.wait(6) + end + U.tap(game, "a") + U.wait(20) + U.tap(game, "a") + U.wait(6) + + local sawOut, sawIn = false, false + for _ = 1, 1500 do + for _, a in ipairs(FieldEffects._anims or {}) do + if a.kind == "teleport_out" then sawOut = true end + if a.kind == "teleport_in" then sawIn = true end + end + if sawIn and not Field.locked then break end + U.wait(1) + end + check(sawOut, "teleport_out ran") + check(sawIn, "teleport_in ran") + check(Field.locked == false, "field unlocked after teleport") + print("[driver] teleport " .. tostring(startMap) .. " -> " .. tostring(session.map)) + check(session.map ~= nil and session.map ~= startMap, "landed on a different map") + U.wait(30) + U.still(game, DIR .. "/after_teleport.png") + end + if not os.getenv("EM_TV_SKIP_TELEPORT") then teleportStage() end + + local beforeHp = mon.maxHp + local beforeEv = Pokemon.evCount(mon) + openStart("bag") + check(BagMenu.isOpen(), "bag opens") + local row + for i, r in ipairs(BagMenu.list()) do + if tonumber(r.id) == ITEM_HP_UP then row = i break end + end + check(row ~= nil, "HP UP in the bag") + if not row then return end + for _ = 1, 30 do + if BagMenu.cursor == row then break end + U.tap(game, "down") + U.wait(6) + end + U.tap(game, "a") + U.wait(30) + for _ = 1, 10 do + if BagMenu.ACTIONS[BagMenu.actionCursor] == "USE" then break end + U.tap(game, "down") + U.wait(6) + end + U.tap(game, "a") + U.wait(60) + check(PartyMenu.open and PartyMenu.mode == "use", "party menu opened for HP UP") + U.tap(game, "a") + U.wait(60) + U.tap(game, "a") + U.wait(30) + check(Pokemon.evCount(mon) == beforeEv + 10, "HP UP added 10 EVs (" .. tostring(Pokemon.evCount(mon)) .. ")") + check(mon.maxHp >= beforeHp, "max HP did not drop") + for _ = 1, 6 do + U.tap(game, "b") + U.wait(20) + end + U.wait(30) + check(Field.locked ~= true, "field not locked after closing menus") + U.still(game, DIR .. "/after_hpup.png") +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/drivers/game3_battle_move_swap.lua b/tests/drivers/game3_battle_move_swap.lua new file mode 100644 index 00000000..cc12ecd8 --- /dev/null +++ b/tests/drivers/game3_battle_move_swap.lua @@ -0,0 +1,10 @@ +local Common = require("tests.drivers.battle_move_swap_common") + +return function(game) + Common.run(game, { + version = "firered", + tag = "frlg", + dir = "/tmp/game3_battle_move_swap", + newGame = { action = "new_game", name = "RED" }, + }) +end diff --git a/tests/drivers/game3_sell_flow_frlg.lua b/tests/drivers/game3_sell_flow_frlg.lua new file mode 100644 index 00000000..f9deec79 --- /dev/null +++ b/tests/drivers/game3_sell_flow_frlg.lua @@ -0,0 +1,112 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_sell_flow_frlg" + +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") .. " game3_sell_flow_frlg failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +local function try(label, fn) + local ok, err = xpcall(fn, debug.traceback) + if not ok then + print("[driver] " .. label .. " error: " .. tostring(err)) + failures = failures + 1 + end + return ok +end + +local function body(game) + local Runtime = require("src.core.game3.runtime") + local Bag = require("src.core.game3.bag") + local BagMenu = require("src.ui.game3.bag_menu") + local ItemsData = require("src.core.game3.items_data") + local C = require("src.core.game3.constants").of("firered") + local session = Runtime.getSession() + if not check(session and session.version == "firered", "firered session") then return end + + session.bag = session.bag or Bag.new() + local POTION = C:require("items", "ITEM_POTION") + local BAG_PACK = C:require("items", "ITEM_POKE_BALL") + Bag.add(session.bag, POTION, 3) + session.money = 1000 + local Q = require("src.core.game3.quest_log_recorder") + + BagMenu.show(session.bag, { session = session, location = "shop" }) + U.wait(30) + local row + for i, r in ipairs(BagMenu.list()) do + if tonumber(r.id) == POTION then row = i break end + end + check(row ~= nil, "potion in the bag") + if not row then return end + for _ = 1, 20 do + if BagMenu.cursor == row then break end + U.tap(game, "down") + U.wait(6) + end + U.tap(game, "a") + U.wait(20) + check(BagMenu.mode == "sell" and BagMenu._sell ~= nil, "sell flow started") + local flow = BagMenu._sell + if not flow then return end + check(flow.state == "qty", "quantity prompt for a stack of 3") + check(flow.text:find("How many would you like to sell", 1, true) ~= nil, "how many text: " .. tostring(flow.text)) + U.wait(10) + U.still(game, DIR .. "/qty.png") + U.tap(game, "up") + U.wait(6) + check(flow.qty == 2, "quantity went to 2") + U.tap(game, "a") + U.wait(10) + check(flow.state == "confirm", "confirm prompt") + check(flow.text:find(tostring(flow.unit * 2), 1, true) ~= nil, + "price text: " .. tostring(flow.text)) + U.still(game, DIR .. "/confirm.png") + local before = session.money + U.tap(game, "a") + U.wait(10) + check(flow.state == "done" or flow.state == nil, "sale committed") + check(session.money == before + flow.unit * 2, "money increased by " .. tostring(flow.unit * 2)) + check(Bag.get(session.bag, POTION) == 1, "two potions removed") + check(flow.text:find(ItemsData.displayName(POTION), 1, true) ~= nil, "turned over text names the item: " .. tostring(flow.text)) + U.still(game, DIR .. "/done.png") + U.tap(game, "a") + U.wait(20) + check(BagMenu.mode == "list", "back to the bag list") + + local SellFlow = require("src.ui.game3.sell_flow") + local cant = SellFlow.start({ itemId = C:require("items", "ITEM_POKE_FLUTE"), session = session, bag = session.bag, owned = 1 }) + check(cant.state == "cant" and cant.state == "cant", "key item refusal text: " .. tostring(cant.text)) + check(pcall(function() cant:draw() end), "cant state draws") + + local CoinsBox = require("src.ui.game3.coins_box") + CoinsBox.show(1, 1, 1234) + check(pcall(function() CoinsBox.draw() end), "coins box draws on FireRed") + U.wait(2) + U.still(game, DIR .. "/coins.png") + CoinsBox.hide() + for _ = 1, 6 do + U.tap(game, "b") + U.wait(15) + end +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + check(game.boot ~= nil, "boot reached") + try("new_game", function() game:_handleBootAction({ action = "new_game", name = "RED" }) end) + U.wait(30) + try("body", function() body(game) end) + return finish() +end diff --git a/tests/emerald_move_relearner_test.lua b/tests/emerald_move_relearner_test.lua new file mode 100644 index 00000000..755b4209 --- /dev/null +++ b/tests/emerald_move_relearner_test.lua @@ -0,0 +1,65 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq + +local Plans = require("src.import.gba.plans.registry") +local Extract = require("src.import.gba.move_relearner_rse_extract") +local Rse = require("src.ui.game3.rse.move_relearner") +local ui = require("src.core.game3.profiles.emerald.ui") + +local required = {} +for _, p in ipairs(Plans.required(Plans.of("emerald"), "data/generated/gba")) do required[p] = true end +check(required["data/generated/gba/rse/move_relearner/hearts.png"], "emerald plan requires the relearner hearts") +check(required["data/generated/gba/rse/move_relearner/manifest.lua"], "emerald plan requires the relearner manifest") +local frRequired = {} +for _, p in ipairs(Plans.required(Plans.of("firered"), "data/generated/gba")) do frRequired[p] = true end +check(not frRequired["data/generated/gba/rse/move_relearner/hearts.png"], "firered plan does not pull the Emerald relearner") + +local aliases = ui.textAliases +eq(aliases.gText_MonIsTryingToLearnMove, "gText_MoveRelearnerPkmnTryingToLearnMove", "trying-to-learn alias") +eq(aliases.gText_WhichMoveShouldBeForgotten, "gText_MoveRelearnerWhichMoveToForget", "which-move alias") +eq(aliases.gText_MonForgotOldMoveAndMonLearnedNewMove, "gText_MoveRelearnerPkmnForgotMoveAndLearnedNew", "forgot alias") + +local a, j = Rse.heartCounts({ appeal = 40, jam = 30 }) +eq(a, 4, "appeal 40 -> 4 hearts") +eq(j, 3, "jam 30 -> 3 hearts") +a, j = Rse.heartCounts({ appeal = 0xFF, jam = 0xFF }) +eq(a, 0, "appeal 0xFF -> 0 hearts") +eq(j, 0, "jam 0xFF -> 0 hearts") +eq(Rse.WIN.list.left, 19, "move list window left") +eq(Rse.WIN.msg.width, 22, "message window width") + +local ROM_PATH = os.getenv("POKEPORT_EMERALD_ROM") or "../pokeemerald/pokeemerald.gba" +local f = io.open(ROM_PATH, "rb") +local data = f and f:read("*a") +if f then f:close() end +if data and data:sub(0xAD, 0xB0) == "BPEE" then + local GV = require("src.core.GameVersion") + local Versions = require("src.import.gba.versions") + local Rom = require("src.import.gba.rom") + local K = require("src.import.gba.rse.boot_gfx") + GV.set("emerald") + Versions.select("emerald") + local sha = GV.VERSIONS.emerald.sha1 + local rom = assert(Rom.open({ + info = function() return { size = #data, md5 = sha } end, + read = function(_, _, off, len) return data:sub(off + 1, off + len) end, + }, GV.forSha1(sha))) + local files = {} + local cache = { + write = function(_, rel, bytes) files[rel] = bytes; return true end, + read = function(_, rel) return files[rel] end, + } + local ok, m = Extract.run(rom, cache, { game = "emerald" }) + check(ok and m and m.hearts, "extractor ran") + local w, h = K.pngSize(files["data/generated/gba/rse/move_relearner/hearts.png"]) + eq(w, 8, "hearts sheet width") + eq(h, 32, "hearts sheet height") + eq(m.hearts.names.jamFull, 3, "jam full frame") + check(Extract.ready(cache), "extractor ready after run") +else + print("emerald_move_relearner_test: ROM extract checks skipped (no Emerald ROM at " .. ROM_PATH .. ")") +end + +T.finish() diff --git a/tests/engine/game3_battle_move_swap_test.lua b/tests/engine/game3_battle_move_swap_test.lua new file mode 100644 index 00000000..0f7c204b --- /dev/null +++ b/tests/engine/game3_battle_move_swap_test.lua @@ -0,0 +1,73 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local eq = T.eq +love = love or require("tests.love_stub") + +local MoveSwap = require("src.core.game3.battle.move_swap") +local State = require("src.core.game3.battle.state") + +local function mon() + return { + moves = { 10, 20, 30, 40 }, pp = { 1, 2, 3, 4 }, maxPp = { 11, 12, 13, 14 }, + ppBonusesPacked = 1 + 2 * 4 + 3 * 16, + } +end + +local function list(t) return table.concat(t, ",") end + +eq(MoveSwap.canStart({}, { mon = mon() }), true, "four moves can start") +eq(MoveSwap.canStart({ link = true }, { mon = mon() }), false, "link battle blocks swap") +eq(MoveSwap.canStart({}, { mon = { moves = { 10, 0, 0, 0 } } }), false, "one move blocks swap") +eq(MoveSwap.canStart({}, nil), false, "no battler blocks swap") + +eq(MoveSwap.initialCursor(0), 1, "cursor 0 starts on 1") +eq(MoveSwap.initialCursor(3), 0, "other cursor starts on 0") + +eq(MoveSwap.step(1, "left", 4), 0, "left") +eq(MoveSwap.step(0, "left", 4), 0, "left edge") +eq(MoveSwap.step(0, "right", 1), 0, "right clamps to move count") +eq(MoveSwap.step(0, "right", 4), 1, "right") +eq(MoveSwap.step(2, "up", 4), 0, "up") +eq(MoveSwap.step(0, "down", 2), 0, "down clamps to move count") +eq(MoveSwap.step(1, "down", 4), 3, "down") + +local plain = { mon = mon() } +eq(MoveSwap.apply(plain, 1, 3), true, "apply returns true") +eq(list(plain.mon.moves), "30,20,10,40", "moves swapped") +eq(list(plain.mon.pp), "3,2,1,4", "pp swapped") +eq(list(plain.mon.maxPp), "13,12,11,14", "maxPp swapped") +eq(plain.mon.ppBonusesPacked, 3 + 2 * 4 + 1 * 16, "pp bonuses swapped") +eq(MoveSwap.apply(plain, 2, 2), false, "same slot is a no-op") + +local party = mon() +local b = { mon = party, expLockedSlot = 1, expEncoreSlot = 3 } +State.ensureBattleMoves(b) +b.permanentSlots[1] = false +b.sketched = { [3] = true } +local proxy = b.mon +proxy.moves[1] = 99 +proxy.pp[1] = 5 +MoveSwap.apply(b, 1, 3) +eq(list(proxy.moves), "30,20,99,40", "proxy moves swapped") +eq(list(proxy.pp), "3,2,5,4", "proxy pp swapped") +eq(list(party.moves), "30,20,10,40", "party moves swapped with proxy") +eq(list(party.pp), "3,2,1,4", "party pp swapped with proxy") +eq(list(party.maxPp), "13,12,11,14", "party maxPp swapped once") +eq(party.ppBonusesPacked, 3 + 2 * 4 + 1 * 16, "party bonuses swapped once") +eq(b.permanentSlots[3], false, "mimicked flag follows the move") +eq(b.permanentSlots[1], true, "swapped slot keeps permanence") +eq(b.sketched[1], true, "sketched flag follows the move") +eq(b.expLockedSlot, 3, "locked slot follows the move") +eq(b.expEncoreSlot, 1, "encore slot follows the move") + +local party2 = mon() +local t = { mon = party2, transformed = true } +State.ensureBattleMoves(t) +t.mon.moves = { 1, 2, 3, 4 } +t.mon.pp = { 5, 5, 5, 5 } +MoveSwap.apply(t, 1, 2) +eq(list(t.mon.moves), "2,1,3,4", "transformed battler moves swapped") +eq(list(party2.moves), "10,20,30,40", "transformed swap leaves party alone") + +T.finish("game3_battle_move_swap_test") diff --git a/tests/engine/game3_emerald_shared_keys_test.lua b/tests/engine/game3_emerald_shared_keys_test.lua new file mode 100644 index 00000000..d5b7e66d --- /dev/null +++ b/tests/engine/game3_emerald_shared_keys_test.lua @@ -0,0 +1,24 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local eq = T.eq +love = love or require("tests.love_stub") + +local Itemfinder = require("src.core.game3.itemfinder") +local Flags = require("src.core.game3.scripting.flags") + +local rse = { version = "emerald" } +local frlg = { version = "firered" } + +eq(Itemfinder.textKey("nothing", rse), "gText_ItemFinderNothing", "Emerald itemfinder nothing key") +eq(Itemfinder.textKey("nearby", rse), "gText_ItemFinderNearby", "Emerald itemfinder nearby key") +eq(Itemfinder.textKey("onTop", rse), "gText_ItemFinderOnTop", "Emerald itemfinder on top key") +eq(Itemfinder.textKey("nothing", frlg), "gText_NopeTheresNoResponse", "FireRed itemfinder nothing key") +eq(Itemfinder.textKey("nearby", frlg), "gText_ItemfinderResponding", "FireRed itemfinder nearby key") +eq(Itemfinder.textKey("onTop", frlg), "gText_ItemfinderShakingWildly", "FireRed itemfinder on top key") + +local em = Flags.forVersion("emerald").IDS +eq(em.FLAG_SYS_ENC_UP_ITEM, 2221, "Emerald white flute flag") +eq(em.FLAG_SYS_ENC_DOWN_ITEM, 2222, "Emerald black flute flag") + +T.finish("game3_emerald_shared_keys_test") From bafa8b41031d399166345c42364c2a19a80b2d10 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Wed, 30 Sep 2026 16:21:10 -0400 Subject: [PATCH 2/2] couple other things --- src/ui/game3/party_menu.lua | 5 ++++ src/ui/game3/rse/summary_menu.lua | 43 +++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index cf6d6881..45993fde 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -402,6 +402,11 @@ local function right_align_3(n) n = math.floor(tonumber(n) or 0) if n < 0 then n = 0 end if n > 999 then n = 999 end + if isRse() then + -- pokeemerald/src/string_util.c:209 ConvertIntToDecimalStringN + local digits = tostring(n) + return string.rep("{UNK_SPACER}", 3 - #digits) .. digits + end return string.format("%3d", n) end diff --git a/src/ui/game3/rse/summary_menu.lua b/src/ui/game3/rse/summary_menu.lua index 4693649e..b2f9cded 100644 --- a/src/ui/game3/rse/summary_menu.lua +++ b/src/ui/game3/rse/summary_menu.lua @@ -467,6 +467,23 @@ local function drawSkills(m, Sm, mon) end end +local function drawMoveSelector(m, row, endFrame, midFrame) + local y = 40 + (row - 1) * 16 - 8 + for i = 0, 9 do + local x = i * 16 + 89 - 8 + if i == 9 then + local img = Kit.image(m.moveSelect.png) + if img then + local sw, sh = img:getDimensions() + love.graphics.setColor(1, 1, 1, 1) + love.graphics.draw(img, quad(m.moveSelect.png, 0, endFrame * 16, 16, 16, sw, sh), x + 16, y, 0, -1, 1) + end + else + drawFrame(m.moveSelect, i == 0 and endFrame or midFrame, x, y) + end + end +end + -- pokeemerald/src/pokemon_summary_screen.c:3527 local function drawMoves(m, Sm, mon, contest, detail) local moves = moveList(Sm, mon) @@ -497,21 +514,19 @@ local function drawMoves(m, Sm, mon, contest, detail) local cur = Sm._moveCursor or 1 local sel = moves[cur] -- pokeemerald/src/pokemon_summary_screen.c:4112 + local swapSlot = Sm._swapSlot + if (swapSlot ~= nil) ~= st.swapping then + st.swapping = swapSlot ~= nil + st.blink = 0 + end st.blink = ((st.blink or 0) + 1) % 32 - if st.blink <= 24 then - local y = 40 + (cur - 1) * 16 - 8 - for i = 0, 9 do - local x = i * 16 + 89 - 8 - if i == 9 then - local img = Kit.image(m.moveSelect.png) - if img then - local sw, sh = img:getDimensions() - love.graphics.draw(img, quad(m.moveSelect.png, 0, 4 * 16, 16, 16, sw, sh), x + 16, y, 0, -1, 1) - end - else - drawFrame(m.moveSelect, i == 0 and 4 or 5, x, y) - end - end + local visible = st.blink <= 24 + if swapSlot then + -- pokeemerald/src/pokemon_summary_screen.c:2043 + drawMoveSelector(m, swapSlot, 6, 7) + if visible then drawMoveSelector(m, cur, 4, 5) end + elseif visible then + drawMoveSelector(m, cur, 4, 5) end if not sel then return end local dw = pageWin(m, "moves", 2)