Files
gen1recomp/src/ui/gen2/MoveTutor.lua
T
2026-08-26 12:04:06 -04:00

163 lines
5.4 KiB
Lua

-- ../pokecrystal/engine/events/move_tutor.asm:1 MoveTutor, the submenu
-- FadeToMenu / ChooseMonToLearnTMHM / CloseSubmenu wraps, and :54
-- CheckCanLearnMoveTutorMove, whose refusals print into a Textbox over the
-- party list (:101-103 `menu_coords 0, 12, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1`).
--
-- ChooseMonToLearnTMHM (../pokecrystal/engine/items/tmhm.asm:73) is
-- InitPartyMenuWithCancel with PARTYMENUACTION_TEACH_TMHM, which is the list
-- src/ui/gen2/PartyMenu.lua already draws when it is handed `tmhm`.
local Chrome = require("src.ui.gen2.Chrome")
local Happiness = require("src.core.gen2.Happiness")
local Mon = require("src.battle.gen2.Mon")
local PartyMenu = require("src.ui.gen2.PartyMenu")
local Strings = require("src.core.Strings")
local MoveTutor = {}
MoveTutor.__index = MoveTutor
MoveTutor.isOpaque = true
-- ../pokecrystal/data/text/common_2.asm:187 _TMHMNotCompatibleText and
-- common_3.asm:1424 _KnowsMoveText. CopyName1 leaves the MOVE in
-- wStringBuffer2 and GetNickname the nickname in wStringBuffer1.
local NOT_COMPATIBLE = Strings.source("%s is\nnot compatible\vwith %s.")
local KNOWS_MOVE = Strings.source("%s knows\n%s.")
-- CanLearnTMHMMove's bitfield: `add_mt` gives the three tutor moves TMHM
-- flags 58-60 (../pokecrystal/constants/item_constants.asm:295-307), which
-- the cache splits out as `tutorMoves`.
function MoveTutor.canLearn(species, moveId)
if type(species) ~= "table" or not moveId then return false end
for _, id in ipairs(species.tmhm or {}) do
if id == moveId then return true end
end
for _, id in ipairs(species.tutorMoves or {}) do
if id == moveId then return true end
end
return false
end
-- The same bitfield as a pokemon.lua view, for PartyMenu's ABLE / NOT ABLE
-- column (src/ui/gen2/PartyMenu.lua:735, which walks `tmhm` only).
function MoveTutor.speciesView(pokemon)
local cache = {}
return setmetatable({}, { __index = function(_, key)
local hit = cache[key]
if hit ~= nil then return hit or nil end
local def = pokemon and pokemon[key]
if type(def) ~= "table" then
cache[key] = false
return nil
end
local merged = {}
for field, value in pairs(def) do merged[field] = value end
local list = {}
for _, id in ipairs(def.tmhm or {}) do list[#list + 1] = id end
for _, id in ipairs(def.tutorMoves or {}) do list[#list + 1] = id end
merged.tmhm = list
cache[key] = merged
return merged
end })
end
-- opts: move, moveName, onDone(learned)
function MoveTutor.new(game, opts)
opts = opts or {}
local self = setmetatable({}, MoveTutor)
self.game = game
local data = (game and game.data) or {}
self.move = opts.move
self.moveName = opts.moveName or opts.move or "?"
self.onDone = opts.onDone
self.pokemon = opts.pokemon or data.pokemon
self.view = MoveTutor.speciesView(self.pokemon)
self.list = self:buildList()
return self
end
function MoveTutor:wantsFillScale() return true end
function MoveTutor:drawsWidescreen() return true end
function MoveTutor:buildList()
return PartyMenu.new(self.game, {
prompt = "teach",
tmhm = { move = self.move },
pokemon = self.view,
onChoose = function(_, mon) self:picked(mon) end,
onCancel = function() self:finish(false) end,
})
end
function MoveTutor:say(body, onDone)
if self.game and self.game.say then return self.game:say(body, onDone) end
if onDone then onDone() end
end
function MoveTutor:playSfx(name)
local world = self.game and self.game.world
if world and world.playSfxNamed then world:playSfxNamed(name) end
end
-- CheckCanLearnMoveTutorMove (../pokecrystal/engine/events/move_tutor.asm:54);
-- all three failures fall back into the list, `jr nc, .loop` at :24.
function MoveTutor:picked(mon)
if not mon then return end
local name = Mon.displayName(mon)
if not MoveTutor.canLearn(self.view[mon.species], self.move) then
self:playSfx("Sfx_Wrong")
return self:say(Strings(NOT_COMPATIBLE, self.moveName, name))
end
-- ../pokecrystal/engine/pokemon/knows_move.asm:16 sets carry and prints,
-- which is the same .didnt_learn arm.
for _, move in ipairs(mon.moves or {}) do
if move.id == self.move then
return self:say(Strings(KNOWS_MOVE, name, self.moveName))
end
end
local game = self.game
if not (game and game.learnMoveOn) then return self:finish(false) end
game:learnMoveOn(mon, self.move, function(learned)
if not learned then return end
-- :87-88 `ld c, HAPPINESS_LEARNMOVE / callfar ChangeHappiness`. The 4000
-- coins are the script's own takecoins (maps/GoldenrodCity.asm:124).
Happiness.change(mon, "LEARNMOVE")
self:finish(true)
end)
end
function MoveTutor:finish(learned)
if self.done then return end
self.done = true
local stack = self.game and self.game.stack
if stack then stack:pop() end
if self.onDone then self.onDone(learned) end
end
function MoveTutor:update(dt)
if self.done then return end
self.list:update(dt)
end
function MoveTutor:drawPanel()
self.list:drawPanel()
end
function MoveTutor:draw()
self.list:draw()
end
-- PartyMenu:drawWidescreen's own blit: the party page is white to the window
-- edge, at the one integer scale Chrome.fitScale picks.
function MoveTutor:drawWidescreen(winW, winH)
local G = love.graphics
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
G.scale(scale, scale)
self:drawPanel()
G.pop()
end
return MoveTutor