Merge pull request #2587 from bryanthaboi/dev

several bugs fixed
This commit is contained in:
bryanthaboi
2026-09-30 16:26:21 -04:00
committed by GitHub
47 changed files with 2202 additions and 73 deletions
+1
View File
@@ -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") },
+74
View File
@@ -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
+74 -5
View File
@@ -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
+1
View File
@@ -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,
+7
View File
@@ -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
+8 -4
View File
@@ -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
+19 -3
View File
@@ -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 = {
+21 -8
View File
@@ -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, {})
+12 -1
View File
@@ -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 = {}
+11 -3
View File
@@ -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
+1 -1
View File
@@ -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
+6
View File
@@ -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]
+3
View File
@@ -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,
+3 -1
View File
@@ -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
+1
View File
@@ -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
@@ -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 },
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -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 = {}
@@ -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
+10 -2
View File
@@ -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" },
}
+25 -2
View File
@@ -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"),
+8 -2
View File
@@ -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)
+6 -4
View File
@@ -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()
+8
View File
@@ -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
+5 -3
View File
@@ -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
+21 -5
View File
@@ -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
@@ -1258,7 +1263,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 +1808,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
+3 -1
View File
@@ -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)
+453
View File
@@ -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
+8
View File
@@ -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 = {}
+37 -19
View File
@@ -465,11 +465,22 @@ 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
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
@@ -503,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)
@@ -566,6 +575,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 +587,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
+65 -7
View File
@@ -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)
+183
View File
@@ -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
+109
View File
@@ -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
+10
View File
@@ -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
+39
View File
@@ -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
+70
View File
@@ -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
+57
View File
@@ -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
+146
View File
@@ -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
+113
View File
@@ -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
+67
View File
@@ -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
+178
View File
@@ -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
+10
View File
@@ -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
+112
View File
@@ -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
+65
View File
@@ -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()
@@ -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")
@@ -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")