mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
The new experience (#201)
* new launcher and save converts and pipeline * fixing bugs
This commit is contained in:
@@ -331,6 +331,12 @@ end
|
||||
|
||||
local function useItem(game, battle, id, list)
|
||||
local def = game.data.items[id]
|
||||
-- ItemUseTMHM checks wIsInBattle before BootedUpTMText
|
||||
if battle and def and def.machine then
|
||||
local _, payload = ItemEffects.use(game.data, game.save, id, nil, battle)
|
||||
showMessages(game, payload)
|
||||
return
|
||||
end
|
||||
if ItemEffects.needsTarget(id, def) and not ItemEffects.isBall(id) then
|
||||
-- TMs/HMs boot up and announce their move before the target picker
|
||||
-- (ItemUseTMHM: BootedUpTMText / BootedUpHMText + TeachMachineMoveText)
|
||||
|
||||
+98
-21
@@ -3,9 +3,11 @@
|
||||
-- plus CHANGE BOX.
|
||||
|
||||
local Boxes = require("src.pokemon.Boxes")
|
||||
local Font = require("src.render.Font")
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local Menu = require("src.ui.Menu")
|
||||
local Party = require("src.pokemon.Party")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local BoxMenu = {}
|
||||
|
||||
@@ -14,6 +16,11 @@ local function monLabel(game, mon)
|
||||
return ("%s :L%d"):format(mon.nickname or def.name, mon.level)
|
||||
end
|
||||
|
||||
local function monName(game, mon)
|
||||
local def = game.data.pokemon[mon.species]
|
||||
return mon.nickname or def.name
|
||||
end
|
||||
|
||||
-- Per-mon submenu (bills_pc.asm DisplayDepositWithdrawMenu): the chosen
|
||||
-- action + STATS + CANCEL. STATS shows the status screen and returns
|
||||
-- here; CANCEL/B goes back to the list.
|
||||
@@ -31,8 +38,26 @@ local function monSubmenu(game, action, mon, onAction)
|
||||
}, { tx = 9, ty = 10, tw = 11, th = 8, noSound = true }))
|
||||
end
|
||||
|
||||
-- After a successful transfer: close the mon list (BoxMenu stays beneath
|
||||
-- via keepOpen) and show the taken-out / stored text (jp BillsPCMenu).
|
||||
local function afterTransfer(game, list, text)
|
||||
list:close()
|
||||
game.stack:push(TextBox.new(game, text))
|
||||
end
|
||||
|
||||
local function withdraw(game)
|
||||
local box = Boxes.active(game.save)
|
||||
local t = game.data.text
|
||||
if #box == 0 then
|
||||
game.stack:push(TextBox.new(game, t._NoMonText
|
||||
or "What? There are\nno POKéMON here!"))
|
||||
return
|
||||
end
|
||||
if #game.save.party >= Party.MAX then
|
||||
game.stack:push(TextBox.new(game, t._CantTakeMonText
|
||||
or "You can't take\nany more POKéMON.\fDeposit POKéMON\nfirst."))
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for i, mon in ipairs(box) do
|
||||
table.insert(items, { label = monLabel(game, mon), value = i })
|
||||
@@ -49,13 +74,29 @@ local function withdraw(game)
|
||||
end
|
||||
table.remove(box, item.value)
|
||||
table.insert(game.save.party, mon)
|
||||
list:close()
|
||||
local name = monName(game, mon)
|
||||
game.stringBuffer = name
|
||||
require("src.core.Sound").playCry(game.data, mon.species)
|
||||
afterTransfer(game, list, t._MonIsTakenOutText
|
||||
or (name .. " is\ntaken out.\vGot " .. name .. "."))
|
||||
end)
|
||||
end,
|
||||
}))
|
||||
end
|
||||
|
||||
local function deposit(game)
|
||||
local t = game.data.text
|
||||
if #game.save.party <= 1 then
|
||||
game.stack:push(TextBox.new(game, t._CantDepositLastMonText
|
||||
or "You can't deposit\nthe last POKéMON!"))
|
||||
return
|
||||
end
|
||||
local box = Boxes.active(game.save)
|
||||
if #box >= Boxes.CAPACITY then
|
||||
game.stack:push(TextBox.new(game, t._BoxFullText
|
||||
or "Oops! This Box is\nfull of POKéMON."))
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for i, mon in ipairs(game.save.party) do
|
||||
table.insert(items, { label = monLabel(game, mon), value = i })
|
||||
@@ -69,40 +110,50 @@ local function deposit(game)
|
||||
list.footer = "You need at least\none POKéMON!"
|
||||
return
|
||||
end
|
||||
local box = Boxes.active(game.save)
|
||||
if #box >= Boxes.CAPACITY then
|
||||
local active = Boxes.active(game.save)
|
||||
if #active >= Boxes.CAPACITY then
|
||||
list.footer = ("BOX %d is full!"):format(game.save.currentBox)
|
||||
return
|
||||
end
|
||||
table.remove(game.save.party, item.value)
|
||||
table.insert(box, mon)
|
||||
list:close()
|
||||
table.insert(active, mon)
|
||||
local name = monName(game, mon)
|
||||
game.stringBuffer = name
|
||||
game.boxNumString = tostring(game.save.currentBox)
|
||||
require("src.core.Sound").playCry(game.data, mon.species)
|
||||
afterTransfer(game, list, t._MonWasStoredText
|
||||
or (name .. " was\nstored in Box " .. game.boxNumString .. "."))
|
||||
end)
|
||||
end,
|
||||
}))
|
||||
end
|
||||
|
||||
-- RELEASE POKéMON (bills_pc.asm .release): confirm, then "Bye [MON]!"
|
||||
-- RELEASE POKéMON (bills_pc.asm BillsPCRelease): confirm, then "Bye [MON]!".
|
||||
-- Index by list.index (not stale item.value) after removeCurrent (#171).
|
||||
local function release(game)
|
||||
local box = Boxes.active(game.save)
|
||||
local t = game.data.text
|
||||
if #box == 0 then
|
||||
game.stack:push(TextBox.new(game, t._NoMonText
|
||||
or "What? There are\nno POKéMON here!"))
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for i, mon in ipairs(box) do
|
||||
table.insert(items, { label = monLabel(game, mon), value = i })
|
||||
end
|
||||
game.stack:push(ListMenu.new(game,
|
||||
("BOX %d (RELEASE)"):format(game.save.currentBox), items, {
|
||||
onChoose = function(item, list)
|
||||
local mon = box[item.value]
|
||||
onChoose = function(_, list)
|
||||
local mon = box[list.index]
|
||||
if not mon then return end
|
||||
local def = game.data.pokemon[mon.species]
|
||||
local name = mon.nickname or def.name
|
||||
local name = monName(game, mon)
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
"Once released,\n" .. name .. " is\ngone forever. OK?", function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then return end
|
||||
table.remove(box, item.value)
|
||||
table.remove(box, list.index)
|
||||
require("src.core.Sound").playCry(game.data, mon.species)
|
||||
game.stack:push(TextBox.new(game,
|
||||
("%s was\nreleased outside.\fBye %s!"):format(name, name)))
|
||||
@@ -129,7 +180,6 @@ local function changeBox(game)
|
||||
-- the original asks BEFORE switching ("When you change a #MON
|
||||
-- BOX, data will be saved. OK?"); declining aborts the change
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
"When you change a\nPOKéMON BOX, data\nwill be saved. OK?", function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
@@ -143,21 +193,48 @@ local function changeBox(game)
|
||||
}))
|
||||
end
|
||||
|
||||
-- bills_pc.asm BillsPCMenu chrome: What? text box + BOX No. overlay
|
||||
local function drawChrome(game)
|
||||
Font.drawBox(0, 12, 20, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw("What?", 8, 112)
|
||||
Font.drawBox(9, 14, 11, 4)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw("BOX No.", 80, 128)
|
||||
local n = game.save.currentBox or 1
|
||||
if n >= 10 then
|
||||
Font.draw(tostring(n), 136, 128)
|
||||
else
|
||||
Font.draw(tostring(n), 144, 128)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function BoxMenu.new(game)
|
||||
Boxes.ensure(game.save)
|
||||
-- bills_pc.asm BillsPCMenu: TextBoxBorder at (0,0) with interior
|
||||
-- 12x10 → total 14x12. "CHANGE BOX" is 10 tiles and needs the
|
||||
-- full interior (cursor col + label); the old tw=12 right-side box
|
||||
-- drew the final glyph on the border.
|
||||
return Menu.new(game, {
|
||||
{ label = "WITHDRAW", onSelect = function() withdraw(game) end },
|
||||
{ label = "DEPOSIT", onSelect = function() deposit(game) end },
|
||||
{ label = "RELEASE", onSelect = function() release(game) end },
|
||||
{ label = "CHANGE BOX", onSelect = function() changeBox(game) end },
|
||||
-- 12x10 → total 14x12. "CHANGE BOX" / "WITHDRAW <PK><MN>" need the
|
||||
-- full interior (cursor col + label). keepOpen so WITHDRAW/DEPOSIT/
|
||||
-- RELEASE/CHANGE BOX leave this menu underneath (jp BillsPCMenu).
|
||||
local menu = Menu.new(game, {
|
||||
{ label = "WITHDRAW <PK><MN>", keepOpen = true,
|
||||
onSelect = function() withdraw(game) end },
|
||||
{ label = "DEPOSIT <PK><MN>", keepOpen = true,
|
||||
onSelect = function() deposit(game) end },
|
||||
{ label = "RELEASE <PK><MN>", keepOpen = true,
|
||||
onSelect = function() release(game) end },
|
||||
{ label = "CHANGE BOX", keepOpen = true,
|
||||
onSelect = function() changeBox(game) end },
|
||||
{ label = "SEE YA!" },
|
||||
-- Bill's PC runs silent end to end (BIT_NO_MENU_BUTTON_SOUND,
|
||||
-- engine/menus/pokemon_pc.asm)
|
||||
}, { tx = 0, ty = 0, tw = 14, th = 12, noSound = true })
|
||||
local baseDraw = menu.draw
|
||||
function menu:draw()
|
||||
baseDraw(self)
|
||||
drawChrome(game)
|
||||
end
|
||||
return menu
|
||||
end
|
||||
|
||||
return BoxMenu
|
||||
|
||||
+12
-7
@@ -1,4 +1,4 @@
|
||||
-- YES/NO choice box (top-left of the text box area, like the original).
|
||||
-- YES/NO choice box (InitYesNoTextBoxParameters: above the text box, right).
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Theme = require("src.ui.Theme")
|
||||
@@ -14,6 +14,11 @@ function ChoiceBox.new(game, onChoose, opts)
|
||||
self.index = (opts and opts.defaultNo) and 2 or 1
|
||||
-- BIT_NO_MENU_BUTTON_SOUND: PC-session prompts stay silent
|
||||
self.noSound = (opts and opts.noSound) or false
|
||||
local box = Theme.choiceBox
|
||||
self.tx = (opts and opts.tx) or box.tx
|
||||
self.ty = (opts and opts.ty) or box.ty
|
||||
self.tw = (opts and opts.tw) or box.tw
|
||||
self.th = (opts and opts.th) or box.th
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -38,13 +43,13 @@ function ChoiceBox:update(dt)
|
||||
end
|
||||
|
||||
function ChoiceBox:draw()
|
||||
local box = Theme.choiceBox
|
||||
Font.drawBox(box.tx, box.ty, box.tw, box.th)
|
||||
local tx, ty, tw, th = self.tx, self.ty, self.tw, self.th
|
||||
Font.drawBox(tx, ty, tw, th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw("YES", (box.tx + 2) * 8, (box.ty + 1) * 8)
|
||||
Font.draw("NO", (box.tx + 2) * 8, (box.ty + 3) * 8)
|
||||
Font.drawCode(Theme.cursor, (box.tx + 1) * 8,
|
||||
(box.ty + (self.index == 1 and 1 or 3)) * 8)
|
||||
Font.draw("YES", (tx + 2) * 8, (ty + 1) * 8)
|
||||
Font.draw("NO", (tx + 2) * 8, (ty + 3) * 8)
|
||||
Font.drawCode(Theme.cursor, (tx + 1) * 8,
|
||||
(ty + (self.index == 1 and 1 or 3)) * 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
+10
-5
@@ -37,8 +37,12 @@ function ListMenu.new(game, title, items, opts)
|
||||
-- text box, a money box sits top-right, and the list shortens to
|
||||
-- clear them (DisplayPokemartDialogue_'s screen)
|
||||
self.dialogue = opts.dialogue
|
||||
-- PC item lists (players_pc.asm): PrintListMenuEntries shows 4 names
|
||||
-- and PrintText footers ("How many?", stored/withdrew) use the standard
|
||||
-- bottom text box — same row budget as the mart, without the money box.
|
||||
self.messageBox = opts.messageBox
|
||||
self.money = opts.money -- () -> current money for the box
|
||||
self.rows = opts.dialogue and 4 or ROWS
|
||||
self.rows = opts.rows or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -137,8 +141,10 @@ function ListMenu:draw()
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local money = ("¥%d"):format(self.money and self.money() or 0)
|
||||
Font.draw(money, 152 - Font.width(money), 8)
|
||||
-- the clerk's line in the standard bottom text box; long prompts
|
||||
-- wrap and keep their last two lines, like the GB's scrolled box
|
||||
end
|
||||
if self.dialogue or (self.messageBox and self.footer) then
|
||||
-- standard bottom text box (PrintText); long prompts wrap and keep
|
||||
-- their last two lines, like the GB's scrolled box (#115/#174)
|
||||
Font.drawBox(0, 12, 20, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
if self.footer then
|
||||
@@ -153,8 +159,7 @@ function ListMenu:draw()
|
||||
end
|
||||
end
|
||||
elseif self.footer then
|
||||
-- PC deposit/withdraw footers use "\n"; draw the last two lines so
|
||||
-- long item names are not clipped at the screen edge (#115).
|
||||
-- bare footer (bag money line, etc.)
|
||||
local flat = {}
|
||||
for _, page in ipairs(require("src.render.TextBox").paginate(self.footer)) do
|
||||
for _, line in ipairs(page) do flat[#flat + 1] = line end
|
||||
|
||||
+24
-12
@@ -22,6 +22,8 @@ function MoveLearnMenu.new(game, mon, newMoveId, onDone)
|
||||
self.newMoveId = newMoveId
|
||||
self.onDone = onDone
|
||||
self.index = 1
|
||||
-- forget-list UI only after TryingToLearn YES (learn_move.asm .loop)
|
||||
self.selecting = false
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -31,24 +33,32 @@ end
|
||||
|
||||
-- TryingToLearnText + yes/no (learn_move.asm TryingToLearn): NO offers
|
||||
-- AbandonLearning, whose own NO loops back here (DontAbandonLearning).
|
||||
-- Use TextBox opts.choice so YES/NO overlays the still-visible prompt;
|
||||
-- pushing ChoiceBox from onDone pops the text first and leaves YES/NO
|
||||
-- on "Which move should be forgotten?" (#173).
|
||||
function MoveLearnMenu:enter()
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local game = self.game
|
||||
local mdef = game.data.moves[self.newMoveId]
|
||||
local name = self:monName()
|
||||
self.selecting = false
|
||||
game.stack:push(TextBox.new(game,
|
||||
("%s is\ntrying to learn\v%s!\fBut, %s\ncan't learn more\vthan 4 moves!\f")
|
||||
:format(name, mdef.name, name) ..
|
||||
("Delete an older\nmove to make room\vfor %s?"):format(mdef.name),
|
||||
function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then self:confirmAbandon() end
|
||||
end))
|
||||
end))
|
||||
nil, {
|
||||
choice = function(yes)
|
||||
if yes then
|
||||
self.selecting = true
|
||||
else
|
||||
self:confirmAbandon()
|
||||
end
|
||||
end,
|
||||
}))
|
||||
end
|
||||
|
||||
function MoveLearnMenu:update(dt)
|
||||
if not self.selecting then return end
|
||||
local input = self.game.input
|
||||
local n = #self.mon.moves + 1 -- moves + CANCEL
|
||||
if input:wasPressed("up") then
|
||||
@@ -82,15 +92,15 @@ end
|
||||
-- (DontAbandonLearning)
|
||||
function MoveLearnMenu:confirmAbandon()
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local game = self.game
|
||||
local mdef = game.data.moves[self.newMoveId]
|
||||
self.selecting = false
|
||||
game.stack:push(TextBox.new(game,
|
||||
("Abandon learning\n%s?"):format(mdef.name), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if yes then self:finish(false) else self:enter() end
|
||||
end))
|
||||
end))
|
||||
("Abandon learning\n%s?"):format(mdef.name), nil, {
|
||||
choice = function(yes)
|
||||
if yes then self:finish(false) else self:enter() end
|
||||
end,
|
||||
}))
|
||||
end
|
||||
|
||||
function MoveLearnMenu:finish(learned)
|
||||
@@ -98,6 +108,7 @@ function MoveLearnMenu:finish(learned)
|
||||
local game = self.game
|
||||
local name = self:monName()
|
||||
local mdef = game.data.moves[self.newMoveId]
|
||||
self.selecting = false
|
||||
game.stack:pop()
|
||||
local msg
|
||||
if learned then
|
||||
@@ -114,6 +125,7 @@ function MoveLearnMenu:finish(learned)
|
||||
end
|
||||
|
||||
function MoveLearnMenu:draw()
|
||||
if not self.selecting then return end
|
||||
-- single-spaced move list box (TryingToLearn: TextBoxBorder at 4,7)
|
||||
-- plus the port's extra CANCEL row
|
||||
Font.drawBox(4, 5, 16, 7)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
-- bottom line like pokered's.
|
||||
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local Tilt = require("src.render.Tilt")
|
||||
local GBCFX = require("src.render.GBCFX")
|
||||
local Zoom = require("src.render.Zoom")
|
||||
@@ -193,6 +194,15 @@ local function buildRows(game)
|
||||
local o = g.save.options
|
||||
o.tilt = wrapIndex((o.tilt or 0) + dir, 4)
|
||||
Tilt.setLevel(o.tilt)
|
||||
-- tilt and a mod's world pipeline are two answers to the same
|
||||
-- question; turning this on switches that off (Pipelines does the
|
||||
-- same in the other direction)
|
||||
if o.tilt > 0 then
|
||||
for _, entry in ipairs(Pipelines.list()) do
|
||||
if entry.def.drawWorld then Pipelines.setLevel(entry.id, 0) end
|
||||
end
|
||||
Pipelines.syncOptions(o)
|
||||
end
|
||||
return true
|
||||
end },
|
||||
{ id = "gbcfx", label = "GBC FX",
|
||||
@@ -295,6 +305,26 @@ local function buildRows(game)
|
||||
end
|
||||
rows = filtered
|
||||
end
|
||||
-- A mod's render pipelines are display modes like TILT, so their rows sit
|
||||
-- with it rather than at the end of the list where a mod's own
|
||||
-- ui.options.rows additions land. Nothing registered means nothing
|
||||
-- spliced, so a vanilla install sees the list it always had.
|
||||
local pipelineRows = Pipelines.rows(game)
|
||||
if pipelineRows[1] then
|
||||
local merged = {}
|
||||
for _, row in ipairs(rows) do
|
||||
merged[#merged + 1] = row
|
||||
if row.id == "tilt" then
|
||||
for _, extra in ipairs(pipelineRows) do merged[#merged + 1] = extra end
|
||||
end
|
||||
end
|
||||
-- no TILT row to anchor to (a future build could drop it): append
|
||||
-- rather than silently lose the modes
|
||||
if #merged == #rows then
|
||||
for _, extra in ipairs(pipelineRows) do merged[#merged + 1] = extra end
|
||||
end
|
||||
rows = merged
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
|
||||
+71
-49
@@ -1,8 +1,11 @@
|
||||
-- Party menu: list the party, choose a member.
|
||||
-- Modes:
|
||||
-- default: A -> submenu (STATS / SWITCH order / CANCEL)
|
||||
-- opts.onSwitch: A -> hand the chosen mon to the callback (battle
|
||||
-- switch, item targeting via opts.pickOnly)
|
||||
-- default: A -> submenu (STATS / SWITCH / field moves)
|
||||
-- opts.onSwitch + opts.battle (voluntary PKMN): A -> SWITCH / STATS /
|
||||
-- CANCEL (core.asm PartyMenuOrRockOrRun), then onSwitch on SWITCH
|
||||
-- opts.onSwitch + opts.forceSwitch: A -> onSwitch immediately
|
||||
-- (ChooseNextMon / SHIFT free-switch)
|
||||
-- opts.pickOnly + opts.onSwitch: A -> onSwitch (item / script target)
|
||||
-- opts.onCancel: fired when the menu closes without a pick (B)
|
||||
-- Pops itself on B.
|
||||
|
||||
@@ -125,6 +128,7 @@ function PartyMenu.new(game, opts)
|
||||
self.onSwitch = opts.onSwitch
|
||||
self.onCancel = opts.onCancel
|
||||
self.pickOnly = opts.pickOnly
|
||||
self.forceSwitch = opts.forceSwitch
|
||||
self.battle = opts.battle
|
||||
self.party = opts.party -- link battles pass their clamped copies
|
||||
self.swapFrom = nil
|
||||
@@ -156,7 +160,17 @@ function PartyMenu:update(dt)
|
||||
-- hook-injected entries carry a callback instead of an action id
|
||||
entry.onSelect(mon, self.game)
|
||||
elseif action == "stats" then
|
||||
-- battle and field alike return to the party list afterwards
|
||||
-- (core.asm .partyMenuWasSelected)
|
||||
Screens.push(self.game, "SummaryMenu", mon)
|
||||
elseif action == "battle_switch" then
|
||||
self.game.stack:pop()
|
||||
self.onSwitch(mon)
|
||||
return
|
||||
elseif action == "cancel" then
|
||||
self.game.stack:pop()
|
||||
if self.onCancel then self.onCancel() end
|
||||
return
|
||||
elseif action == "switch" then
|
||||
self.swapFrom = self.index
|
||||
elseif action == "fly" then
|
||||
@@ -341,58 +355,66 @@ function PartyMenu:update(dt)
|
||||
require("src.core.Sound").play(self.game.data, "Swap")
|
||||
end
|
||||
self.swapFrom = nil
|
||||
elseif self.onSwitch then
|
||||
elseif self.onSwitch and (self.forceSwitch or self.pickOnly or not self.battle) then
|
||||
self.game.stack:pop()
|
||||
self.onSwitch(mon)
|
||||
else
|
||||
self.submenu = true
|
||||
self.subIndex = 1
|
||||
-- STATS/SWITCH plus this mon's field moves (start_sub_menus.asm
|
||||
-- builds the same dynamic list)
|
||||
local items = { { label = "STATS", action = "stats" },
|
||||
{ label = "SWITCH", action = "switch" } }
|
||||
local items
|
||||
local ow = self.game.overworld
|
||||
-- Field moves (HMs/TMs) are usable out of battle even when the mon
|
||||
-- is fainted -- Gen 1 does not require HP for Cut/Fly/Surf/etc.
|
||||
-- Battle still excludes this list via `not self.battle`. Softboiled
|
||||
-- can appear for a fainted user; its heal transfer then no-ops.
|
||||
if not self.battle and ow then
|
||||
-- FLY/TELEPORT: CheckIfInOutsideMap (OVERWORLD + PLATEAU —
|
||||
-- Route 23 / Indigo Plateau outdoor), not OVERWORLD alone (#83)
|
||||
local outside = Map.isOutside(ow.map.def,
|
||||
FieldDefaults.field(self.game.data, "outsideTilesets"))
|
||||
for _, mv in ipairs(mon.moves) do
|
||||
if mv.id == "FLY" and outside
|
||||
and self.game.save.inventory.THUNDERBADGE then
|
||||
table.insert(items, { label = "FLY", action = "fly" })
|
||||
elseif mv.id == "FLASH" and ow.dark
|
||||
and self.game.save.inventory.BOULDERBADGE then
|
||||
table.insert(items, { label = "FLASH", action = "flash" })
|
||||
elseif mv.id == "CUT" and self.game.save.inventory.CASCADEBADGE then
|
||||
-- CUT/SURF/STRENGTH are party-menu field moves too
|
||||
-- (start_sub_menus.asm .outOfBattleMovePointers); listed here
|
||||
-- with the same list-time badge filter this file already uses
|
||||
-- for FLY/FLASH. The facing-tile/activation check happens on
|
||||
-- selection (useCutFieldMove/useSurfFieldMove).
|
||||
table.insert(items, { label = "CUT", action = "cut" })
|
||||
elseif mv.id == "SURF" and self.game.save.inventory.SOULBADGE then
|
||||
table.insert(items, { label = "SURF", action = "surf" })
|
||||
elseif mv.id == "STRENGTH" and self.game.save.inventory.RAINBOWBADGE then
|
||||
table.insert(items, { label = "STRENGTH", action = "strength" })
|
||||
elseif mv.id == "SOFTBOILED" then
|
||||
table.insert(items, { label = "SOFTBOILED", action = "softboiled" })
|
||||
elseif mv.id == "TELEPORT" and outside then
|
||||
-- TELEPORT works only OUTDOORS (start_sub_menus.asm
|
||||
-- .teleport -> CheckIfInOutsideMap); dark maps don't
|
||||
-- block it
|
||||
table.insert(items, { label = "TELEPORT", action = "escape" })
|
||||
elseif mv.id == "DIG" and DIG_TILESETS[ow.map.def.tileset]
|
||||
and ow.map.id ~= "AGATHAS_ROOM" then
|
||||
-- DIG runs ItemUseEscapeRope (.dig sets wCurItem =
|
||||
-- ESCAPE_ROPE): usable in the dungeon tilesets of
|
||||
-- escape_rope_tilesets.asm minus Agatha's room, even in
|
||||
-- the dark (Rock Tunnel)
|
||||
table.insert(items, { label = "DIG", action = "escape" })
|
||||
if self.battle and self.onSwitch then
|
||||
-- SwitchStatsCancelText (core.asm PartyMenuOrRockOrRun)
|
||||
items = { { label = "SWITCH", action = "battle_switch" },
|
||||
{ label = "STATS", action = "stats" },
|
||||
{ label = "CANCEL", action = "cancel" } }
|
||||
else
|
||||
-- STATS/SWITCH plus this mon's field moves (start_sub_menus.asm
|
||||
-- builds the same dynamic list)
|
||||
items = { { label = "STATS", action = "stats" },
|
||||
{ label = "SWITCH", action = "switch" } }
|
||||
-- Field moves (HMs/TMs) are usable out of battle even when the mon
|
||||
-- is fainted -- Gen 1 does not require HP for Cut/Fly/Surf/etc.
|
||||
-- Battle still excludes this list via `not self.battle`. Softboiled
|
||||
-- can appear for a fainted user; its heal transfer then no-ops.
|
||||
if not self.battle and ow then
|
||||
-- FLY/TELEPORT: CheckIfInOutsideMap (OVERWORLD + PLATEAU —
|
||||
-- Route 23 / Indigo Plateau outdoor), not OVERWORLD alone (#83)
|
||||
local outside = Map.isOutside(ow.map.def,
|
||||
FieldDefaults.field(self.game.data, "outsideTilesets"))
|
||||
for _, mv in ipairs(mon.moves) do
|
||||
if mv.id == "FLY" and outside
|
||||
and self.game.save.inventory.THUNDERBADGE then
|
||||
table.insert(items, { label = "FLY", action = "fly" })
|
||||
elseif mv.id == "FLASH" and ow.dark
|
||||
and self.game.save.inventory.BOULDERBADGE then
|
||||
table.insert(items, { label = "FLASH", action = "flash" })
|
||||
elseif mv.id == "CUT" and self.game.save.inventory.CASCADEBADGE then
|
||||
-- CUT/SURF/STRENGTH are party-menu field moves too
|
||||
-- (start_sub_menus.asm .outOfBattleMovePointers); listed here
|
||||
-- with the same list-time badge filter this file already uses
|
||||
-- for FLY/FLASH. The facing-tile/activation check happens on
|
||||
-- selection (useCutFieldMove/useSurfFieldMove).
|
||||
table.insert(items, { label = "CUT", action = "cut" })
|
||||
elseif mv.id == "SURF" and self.game.save.inventory.SOULBADGE then
|
||||
table.insert(items, { label = "SURF", action = "surf" })
|
||||
elseif mv.id == "STRENGTH" and self.game.save.inventory.RAINBOWBADGE then
|
||||
table.insert(items, { label = "STRENGTH", action = "strength" })
|
||||
elseif mv.id == "SOFTBOILED" then
|
||||
table.insert(items, { label = "SOFTBOILED", action = "softboiled" })
|
||||
elseif mv.id == "TELEPORT" and outside then
|
||||
-- TELEPORT works only OUTDOORS (start_sub_menus.asm
|
||||
-- .teleport -> CheckIfInOutsideMap); dark maps don't
|
||||
-- block it
|
||||
table.insert(items, { label = "TELEPORT", action = "escape" })
|
||||
elseif mv.id == "DIG" and DIG_TILESETS[ow.map.def.tileset]
|
||||
and ow.map.id ~= "AGATHAS_ROOM" then
|
||||
-- DIG runs ItemUseEscapeRope (.dig sets wCurItem =
|
||||
-- ESCAPE_ROPE): usable in the dungeon tilesets of
|
||||
-- escape_rope_tilesets.asm minus Agatha's room, even in
|
||||
-- the dark (Rock Tunnel)
|
||||
table.insert(items, { label = "DIG", action = "escape" })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+6
-3
@@ -71,6 +71,7 @@ end
|
||||
local function withdraw(game)
|
||||
local pc = game.save.pcItems
|
||||
game.stack:push(ListMenu.new(game, "WITHDRAW ITEM", buildItems(game, pc), {
|
||||
messageBox = true,
|
||||
onChoose = function(item, list)
|
||||
askQuantity(game, list, pc[item.value] or 1, item.value, function(qty)
|
||||
local Bag = require("src.inventory.Bag")
|
||||
@@ -107,6 +108,7 @@ local function deposit(game)
|
||||
if not Bag.isBadge(id) then depositable[id] = count end
|
||||
end
|
||||
game.stack:push(ListMenu.new(game, "DEPOSIT ITEM", buildItems(game, depositable), {
|
||||
messageBox = true,
|
||||
onChoose = function(item, list)
|
||||
askQuantity(game, list, inv[item.value] or 1, item.value, function(qty)
|
||||
if pcFull(game, pc, item.value) then
|
||||
@@ -126,6 +128,7 @@ end
|
||||
local function toss(game)
|
||||
local pc = game.save.pcItems
|
||||
game.stack:push(ListMenu.new(game, "TOSS ITEM", buildItems(game, pc), {
|
||||
messageBox = true,
|
||||
onChoose = function(item, list)
|
||||
local def = game.data.items[item.value]
|
||||
if (def and def.keyItem) or item.value:find("^HM_") then
|
||||
@@ -161,9 +164,9 @@ function PlayerPC.new(game)
|
||||
{ label = "DEPOSIT ITEM", onSelect = function() deposit(game) end },
|
||||
{ label = "TOSS ITEM", onSelect = function() toss(game) end },
|
||||
{ label = "LOG OFF" },
|
||||
-- the whole PC session runs silent (BIT_NO_MENU_BUTTON_SOUND,
|
||||
-- engine/menus/players_pc.asm PlayersPCMenu)
|
||||
}, { tx = 3, ty = 0, tw = 17, th = 10, noSound = true })
|
||||
-- silent PC session (BIT_NO_MENU_BUTTON_SOUND); players_pc.asm
|
||||
-- PlayersPCMenu TextBoxBorder (0,0) b=8 c=14 → 16x10
|
||||
}, { tx = 0, ty = 0, tw = 16, th = 10, noSound = true })
|
||||
end
|
||||
|
||||
return PlayerPC
|
||||
|
||||
@@ -40,15 +40,19 @@ function QuantityBox:update(dt)
|
||||
end
|
||||
|
||||
function QuantityBox:draw()
|
||||
local w = self.unitPrice and 11 or 7
|
||||
local tx = 20 - w - 1
|
||||
Font.drawBox(tx, 13, w, 3)
|
||||
-- DisplayChooseQuantityMenu (home/list_menu.asm): non-priced box at
|
||||
-- hlcoord 15,9 (interior 3x1); priced at hlcoord 7,9 (interior 11x1).
|
||||
-- TextBoxBorder adds the frame, so outer size is +2 on each axis.
|
||||
local tw = self.unitPrice and 13 or 5
|
||||
local tx = self.unitPrice and 7 or 15
|
||||
local ty = 9
|
||||
Font.drawBox(tx, ty, tw, 3)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local s = ("×%02d"):format(self.qty) -- the multiply glyph tile
|
||||
if self.unitPrice then
|
||||
s = s .. (" ¥%d"):format(self.qty * self.unitPrice)
|
||||
end
|
||||
Font.draw(s, (tx + 1) * 8, 14 * 8)
|
||||
Font.draw(s, (tx + 1) * 8, (ty + 1) * 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ local Theme = {
|
||||
cols = Renderer.WIDTH / 8,
|
||||
rows = Renderer.HEIGHT / 8,
|
||||
textBox = { tx = 0, ty = 12, tw = 20, th = 6, maxCols = 18 },
|
||||
choiceBox = { tx = 0, ty = 7, tw = 6, th = 5 },
|
||||
-- InitYesNoTextBoxParameters / AskName: hlcoord 14, 7 (YES_NO_MENU 4x3)
|
||||
choiceBox = { tx = 14, ty = 7, tw = 6, th = 5 },
|
||||
}
|
||||
|
||||
function Theme.load(data)
|
||||
|
||||
+427
-45
@@ -1,8 +1,7 @@
|
||||
-- Link/in-game trade cinematic (engine/movie/trade.asm, trade2.asm):
|
||||
-- the traded POKéMON rises away with its cry and a goodbye, then the
|
||||
-- received one descends with its cry and "take good care" text.
|
||||
-- A skips the slide animations ahead. Calls onDone() after popping.
|
||||
-- InternalClockTradeAnim (engine/movie/trade.asm): cable-trade cinematic
|
||||
-- used by in-game NPC trades and internally-clocked link trades.
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Sound = require("src.core.Sound")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
@@ -10,13 +9,22 @@ local TradeAnim = {}
|
||||
TradeAnim.__index = TradeAnim
|
||||
TradeAnim.isOpaque = true
|
||||
|
||||
-- SGB: generic whole-screen palette (SET_PAL_GENERIC)
|
||||
function TradeAnim:sgbPalettes(game)
|
||||
return require("src.render.PaletteFX").wholeNamed(game.data, "MEWMON")
|
||||
end
|
||||
|
||||
local SLIDE_FRAMES = 90
|
||||
local REST_Y = 44 -- resting top of the sprite, roughly screen centre
|
||||
local DEFAULT_ART = {
|
||||
gameBoy = "assets/generated/trade/game_boy.png",
|
||||
openCable = "assets/generated/trade/open_cable.png",
|
||||
cableHoriz = "assets/generated/trade/cable_horiz.png",
|
||||
cableConn = "assets/generated/trade/cable_conn.png",
|
||||
cableVert = "assets/generated/trade/cable_vert.png",
|
||||
cableCorner = "assets/generated/trade/cable_corner.png",
|
||||
cableEnd = "assets/generated/trade/cable_end.png",
|
||||
cableBall = "assets/generated/trade/cable_ball.png",
|
||||
cableBallAlt = "assets/generated/trade/cable_ball_alt.png",
|
||||
bubble = "assets/generated/trade/bubble.png",
|
||||
}
|
||||
|
||||
local function tryImage(path)
|
||||
if not path then return nil end
|
||||
@@ -29,11 +37,46 @@ local function nameOf(game, mon)
|
||||
return mon.nickname or (def and def.name) or mon.species
|
||||
end
|
||||
|
||||
local function speciesName(game, mon)
|
||||
local def = game.data.pokemon[mon.species]
|
||||
return (def and def.name) or mon.species
|
||||
end
|
||||
|
||||
local function dexOf(game, mon)
|
||||
local def = game.data.pokemon[mon.species]
|
||||
return def and def.dex or 0
|
||||
end
|
||||
|
||||
local function spriteOf(game, mon)
|
||||
local def = game.data.pokemon[mon.species]
|
||||
return tryImage(def and def.spriteFront)
|
||||
end
|
||||
|
||||
local function expand(game, key, subs)
|
||||
local raw = game.data.text and game.data.text[key]
|
||||
if not raw then return key end
|
||||
for token, value in pairs(subs or {}) do
|
||||
raw = raw:gsub("{" .. token .. "}", value)
|
||||
end
|
||||
return TextBox.substitute(game, raw)
|
||||
end
|
||||
|
||||
-- InternalClockTradeFuncSequence
|
||||
local SEQ = {
|
||||
"show_player",
|
||||
"open_cable",
|
||||
"ball_enter",
|
||||
"transfer_lr",
|
||||
"delay",
|
||||
"went_to",
|
||||
"for_sends",
|
||||
"farewell",
|
||||
"transfer_rl",
|
||||
"open_cable2",
|
||||
"show_enemy",
|
||||
"done",
|
||||
}
|
||||
|
||||
function TradeAnim.new(game, opts)
|
||||
opts = opts or {}
|
||||
local self = setmetatable({}, TradeAnim)
|
||||
@@ -41,62 +84,401 @@ function TradeAnim.new(game, opts)
|
||||
self.sent = opts.sent
|
||||
self.received = opts.received
|
||||
self.onDone = opts.onDone
|
||||
self.enemyName = opts.enemyName or (self.received and self.received.ot) or "TRAINER"
|
||||
self.playerName = (game.save.player and game.save.player.name) or "RED"
|
||||
self.playerOt = opts.playerOt or self.playerName
|
||||
self.playerOtId = opts.playerOtId
|
||||
or (self.sent and self.sent.otId)
|
||||
or (game.save.player and game.save.player.id)
|
||||
or 0
|
||||
self.enemyOtId = opts.enemyOtId
|
||||
or (self.received and self.received.otId)
|
||||
or love.math.random(0, 65535)
|
||||
|
||||
local art = (game.data.field and game.data.field.tradeArt) or DEFAULT_ART
|
||||
self.img = {
|
||||
gameBoy = tryImage(art.gameBoy or DEFAULT_ART.gameBoy),
|
||||
openCable = tryImage(art.openCable or DEFAULT_ART.openCable),
|
||||
cableHoriz = tryImage(art.cableHoriz or DEFAULT_ART.cableHoriz),
|
||||
cableConn = tryImage(art.cableConn or DEFAULT_ART.cableConn),
|
||||
cableVert = tryImage(art.cableVert or DEFAULT_ART.cableVert),
|
||||
cableCorner = tryImage(art.cableCorner or DEFAULT_ART.cableCorner),
|
||||
cableEnd = tryImage(art.cableEnd or DEFAULT_ART.cableEnd),
|
||||
cableBall = tryImage(art.cableBall or DEFAULT_ART.cableBall),
|
||||
cableBallAlt = tryImage(art.cableBallAlt or DEFAULT_ART.cableBallAlt),
|
||||
bubble = tryImage(art.bubble or DEFAULT_ART.bubble),
|
||||
}
|
||||
self.sentSprite = spriteOf(game, self.sent)
|
||||
self.receivedSprite = spriteOf(game, self.received)
|
||||
self.phase = "out"
|
||||
self.recvSprite = spriteOf(game, self.received)
|
||||
|
||||
self.seq = 1
|
||||
self.phase = SEQ[1]
|
||||
self.t = 0
|
||||
self.scx = 0
|
||||
self.ballX = 0
|
||||
self.ballY = 0
|
||||
self.monX = 0
|
||||
self.monY = 0
|
||||
self.flash = false
|
||||
self.monVisible = true
|
||||
self.waitingText = false
|
||||
self.cableFlash = false
|
||||
return self
|
||||
end
|
||||
|
||||
function TradeAnim:enter()
|
||||
Sound.playCry(self.game.data, self.sent.species)
|
||||
Sound.play(self.game.data, "Trade_Machine")
|
||||
end
|
||||
|
||||
function TradeAnim:advance()
|
||||
self.seq = self.seq + 1
|
||||
self.phase = SEQ[self.seq] or "done"
|
||||
self.t = 0
|
||||
self.scx = 0
|
||||
self.sub = nil
|
||||
self.flash = false
|
||||
self.cableFlash = false
|
||||
self.poof = nil
|
||||
if self.phase == "done" then
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone() end
|
||||
elseif self.phase == "show_enemy" then
|
||||
self.monVisible = false
|
||||
elseif self.phase == "transfer_lr" then
|
||||
-- wBaseCoord $54, $1c OAM -> screen (76, 12)
|
||||
self.monX, self.monY = 76, 12
|
||||
elseif self.phase == "transfer_rl" then
|
||||
-- wBaseCoord $64, $44 OAM -> screen (92, 52), right GB on screen
|
||||
self.monX, self.monY = 92, 52
|
||||
self.scx = 160
|
||||
elseif self.phase == "ball_enter" then
|
||||
-- lb bc, $20, $60: b = Y, c = X (Trade_AnimateBallEnteringLinkCable)
|
||||
self.ballX, self.ballY = 0x60, 0x20
|
||||
elseif self.phase == "open_cable" or self.phase == "open_cable2" then
|
||||
-- SCX $a0 -> $f0: cable slides in from the right, open end rests at x64
|
||||
self.scx = 0x50
|
||||
Sound.play(self.game.data, "Heal_HP")
|
||||
end
|
||||
end
|
||||
|
||||
function TradeAnim:say(text, delay, thenFn)
|
||||
self.waitingText = true
|
||||
self.game.stack:push(TextBox.new(self.game, text, function()
|
||||
self.waitingText = false
|
||||
if thenFn then thenFn() else self:advance() end
|
||||
end, { auto = { delay = delay or 80 } }))
|
||||
end
|
||||
|
||||
function TradeAnim:skipHeld()
|
||||
return self.game.input:wasPressed("a") or self.game.input:isDown("a")
|
||||
end
|
||||
|
||||
function TradeAnim:update(dt)
|
||||
local input = self.game.input
|
||||
if self.phase == "out" or self.phase == "in" then
|
||||
self.t = self.t + 1
|
||||
if input:wasPressed("a") then self.t = SLIDE_FRAMES end
|
||||
if self.t < SLIDE_FRAMES then return end
|
||||
if self.phase == "out" then
|
||||
self.phase = "goodbye"
|
||||
self.game.stack:push(TextBox.new(self.game,
|
||||
("Goodbye %s!"):format(nameOf(self.game, self.sent)),
|
||||
function()
|
||||
self.phase = "in"
|
||||
self.t = 0
|
||||
Sound.playCry(self.game.data, self.received.species)
|
||||
end))
|
||||
if self.waitingText or self.phase == "done" then return end
|
||||
local skip = self.game.input:wasPressed("a")
|
||||
self.t = self.t + 1
|
||||
local p = self.phase
|
||||
|
||||
if p == "show_player" then
|
||||
-- slide from SCX $86 -> 0, hold 80, poof, cry (Trade_ShowPlayerMon)
|
||||
if not self.sub then self.sub = "slide" end
|
||||
if self.sub == "slide" then
|
||||
self.scx = math.max(0, 0x86 - self.t * 2)
|
||||
if skip or self.scx <= 0 then
|
||||
self.scx = 0
|
||||
self.sub = "hold"
|
||||
self.t = 0
|
||||
end
|
||||
elseif self.sub == "hold" then
|
||||
if skip or self.t >= 80 then
|
||||
self.sub = "poof"
|
||||
self.t = 0
|
||||
self.monVisible = false
|
||||
Sound.play(self.game.data, "Ball_Poof")
|
||||
end
|
||||
elseif self.sub == "poof" then
|
||||
self.poof = math.max(0, 16 - self.t)
|
||||
if skip or self.t >= 16 then
|
||||
Sound.playCry(self.game.data, self.sent.species)
|
||||
self.sub = nil
|
||||
self:advance()
|
||||
end
|
||||
end
|
||||
|
||||
elseif p == "open_cable" or p == "open_cable2" then
|
||||
-- 20 steps of 4px, matching the SCX loop
|
||||
self.scx = math.max(0, 0x50 - self.t * 4)
|
||||
if skip or self.scx <= 0 then
|
||||
self.scx = 0
|
||||
self:advance()
|
||||
end
|
||||
|
||||
elseif p == "ball_enter" then
|
||||
-- TRADE_BALL_SHAKE then ball rides the cable; X from $60 toward $a0
|
||||
if self.t < 20 then
|
||||
if skip then self.t = 20 end
|
||||
return
|
||||
end
|
||||
local step = self.t - 20
|
||||
if step % 3 == 0 then
|
||||
self.ballX = 0x60 + math.floor(step / 3) * 4
|
||||
self.flash = not self.flash
|
||||
if self.ballX < 0xA0 then Sound.play(self.game.data, "Tink") end
|
||||
end
|
||||
if skip then self.ballX = 0xA0 end
|
||||
if self.ballX >= 0xA0 then self:advance() end
|
||||
|
||||
elseif p == "transfer_lr" then
|
||||
-- scroll left GB off while the mon rides the cable, then the sprite
|
||||
-- itself moves right and down into the right GB (Trade_AnimMonMoveVertical)
|
||||
if self.t <= 80 then
|
||||
self.scx = math.min(160, self.t * 2)
|
||||
self.monX, self.monY = 76, 12
|
||||
elseif self.t <= 80 + 32 then
|
||||
self.monX = 76 + math.floor((self.t - 80) / 8) * 4
|
||||
elseif self.t <= 80 + 64 then
|
||||
self.monX = 92
|
||||
self.monY = 12 + math.floor((self.t - 112) / 8) * 10
|
||||
else
|
||||
self.phase = "takecare"
|
||||
self.game.stack:push(TextBox.new(self.game,
|
||||
("Take good care\nof %s!"):format(nameOf(self.game, self.received)),
|
||||
function()
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone() end
|
||||
end))
|
||||
self:advance()
|
||||
return
|
||||
end
|
||||
if self.t % 8 == 0 then self.cableFlash = not self.cableFlash end
|
||||
if skip then self:advance() end
|
||||
|
||||
elseif p == "transfer_rl" then
|
||||
-- mirror: sprite climbs out of the right GB, then the screen scrolls
|
||||
-- back to the left GB
|
||||
if self.t <= 32 then
|
||||
self.scx = 160
|
||||
self.monX, self.monY = 92, 52 - math.floor(self.t / 8) * 10
|
||||
elseif self.t <= 64 then
|
||||
self.monX = 92 - math.floor((self.t - 32) / 8) * 4
|
||||
self.monY = 12
|
||||
elseif self.t <= 64 + 80 then
|
||||
self.scx = math.max(0, 160 - (self.t - 64) * 2)
|
||||
self.monX, self.monY = 76, 12
|
||||
else
|
||||
self:advance()
|
||||
return
|
||||
end
|
||||
if self.t % 8 == 0 then self.cableFlash = not self.cableFlash end
|
||||
if skip then self:advance() end
|
||||
|
||||
elseif p == "delay" then
|
||||
if skip or self.t >= 100 then self:advance() end
|
||||
|
||||
elseif p == "went_to" then
|
||||
local text = expand(self.game, "_TradeWentToText", {
|
||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
})
|
||||
self:say(text, 200)
|
||||
|
||||
elseif p == "for_sends" then
|
||||
local a = expand(self.game, "_TradeForText", {
|
||||
["RAM:wStringBuffer"] = speciesName(self.game, self.sent),
|
||||
})
|
||||
local b = expand(self.game, "_TradeSendsText", {
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
self.waitingText = true
|
||||
self.game.stack:push(TextBox.new(self.game, a, function()
|
||||
self.game.stack:push(TextBox.new(self.game, b, function()
|
||||
self.waitingText = false
|
||||
self:advance()
|
||||
end, { auto = { delay = 80 } }))
|
||||
end, { auto = { delay = 80 } }))
|
||||
|
||||
elseif p == "farewell" then
|
||||
local a = expand(self.game, "_TradeWavesFarewellText", {
|
||||
["RAM:wLinkEnemyTrainerName"] = self.enemyName,
|
||||
})
|
||||
local b = expand(self.game, "_TradeTransferredText", {
|
||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
self.waitingText = true
|
||||
self.game.stack:push(TextBox.new(self.game, a, function()
|
||||
self.game.stack:push(TextBox.new(self.game, b, function()
|
||||
self.waitingText = false
|
||||
self:advance()
|
||||
end, { auto = { delay = 80 } }))
|
||||
end, { auto = { delay = 80 } }))
|
||||
|
||||
elseif p == "show_enemy" then
|
||||
if self.t == 1 then
|
||||
Sound.play(self.game.data, "Ball_Poof")
|
||||
self.monVisible = true
|
||||
elseif self.t == 20 then
|
||||
Sound.playCry(self.game.data, self.received.species)
|
||||
elseif self.t >= 120 or skip then
|
||||
local text = expand(self.game, "_TradeTakeCareText", {
|
||||
["RAM:wNameBuffer"] = nameOf(self.game, self.received),
|
||||
})
|
||||
self:say(text, 80)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function drawCableHoriz(self, y, x0, x1)
|
||||
if self.img.cableHoriz then
|
||||
love.graphics.draw(self.img.cableHoriz, x0 - (self.scx % 8), y)
|
||||
else
|
||||
love.graphics.setColor(0.2, 0.2, 0.2, 1)
|
||||
love.graphics.rectangle("fill", x0, y + 1, math.max(0, x1 - x0), 6)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function TradeAnim:drawMonInfo(mon, ot, otId, boxTy)
|
||||
-- Trade_PrintPlayerMonInfoText: rows +0/+2/+4/+6 from the box top; the
|
||||
-- No. line replaces part of the top border (hlcoord 5, 0 in trade2.asm)
|
||||
Font.drawBox(4, boxTy, 12, 8)
|
||||
local y0 = boxTy * 8
|
||||
local no = ("No.%03d"):format(dexOf(self.game, mon))
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 56, y0, Font.width(no), 8)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(no, 56, y0)
|
||||
Font.draw(speciesName(self.game, mon), 40, y0 + 16)
|
||||
Font.draw("OT/" .. (ot or "????"), 40, y0 + 32)
|
||||
Font.draw(("IDNo.%05d"):format(otId or 0), 40, y0 + 48)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function TradeAnim:drawIconInBubble(mon, x, y)
|
||||
local spr = (mon.species == self.received.species) and self.recvSprite or self.sentSprite
|
||||
if spr then
|
||||
local sw, sh = spr:getDimensions()
|
||||
local s = 16 / math.max(sw, sh)
|
||||
love.graphics.draw(spr, x, y, 0, s, s)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("fill", x + 4, y + 4, 8, 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
if self.img.bubble then
|
||||
if not self.bubbleQuad then
|
||||
local iw, ih = self.img.bubble:getDimensions()
|
||||
self.bubbleQuad = love.graphics.newQuad(0, 0, 16, 16, iw, ih)
|
||||
self.bubbleQuadAlt = ih >= 32
|
||||
and love.graphics.newQuad(0, 16, 16, 16, iw, ih)
|
||||
or self.bubbleQuad
|
||||
end
|
||||
local q = self.cableFlash and self.bubbleQuadAlt or self.bubbleQuad
|
||||
love.graphics.draw(self.img.bubble, q, x - 8, y - 8)
|
||||
end
|
||||
end
|
||||
|
||||
function TradeAnim:drawGameBoy(x, y)
|
||||
if self.img.gameBoy then
|
||||
love.graphics.draw(self.img.gameBoy, x, y)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", x, y, 48, 64)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function TradeAnim:drawLeftGB()
|
||||
-- cable from GB to the right edge
|
||||
if self.img.cableConn then
|
||||
love.graphics.draw(self.img.cableConn, 88, 32)
|
||||
end
|
||||
drawCableHoriz(self, 32, 96, 160)
|
||||
self:drawGameBoy(40, 24)
|
||||
Font.drawBox(4, 12, 9, 4)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(self.playerName, 40, 112)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function TradeAnim:drawRightGB()
|
||||
drawCableHoriz(self, 32, 0, 112)
|
||||
if self.img.cableCorner then love.graphics.draw(self.img.cableCorner, 112, 32) end
|
||||
if self.img.cableVert then
|
||||
for i = 1, 4 do
|
||||
love.graphics.draw(self.img.cableVert, 120, 40 + (i - 1) * 8)
|
||||
end
|
||||
end
|
||||
if self.img.cableEnd then love.graphics.draw(self.img.cableEnd, 112, 72) end
|
||||
if self.img.cableConn then love.graphics.draw(self.img.cableConn, 104, 72) end
|
||||
self:drawGameBoy(56, 64)
|
||||
Font.drawBox(6, 0, 9, 4)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(self.enemyName, 56, 16)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function TradeAnim:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
local sprite, y
|
||||
if self.phase == "out" then
|
||||
-- the sent mon rises up and off the screen
|
||||
sprite = self.sentSprite
|
||||
y = REST_Y - math.floor((self.t / SLIDE_FRAMES) * (REST_Y + 60))
|
||||
elseif self.phase == "in" or self.phase == "takecare" then
|
||||
-- the received mon descends into place
|
||||
sprite = self.receivedSprite
|
||||
local t = self.phase == "in" and self.t or SLIDE_FRAMES
|
||||
y = -60 + math.floor((t / SLIDE_FRAMES) * (REST_Y + 60))
|
||||
end
|
||||
if sprite and y then
|
||||
local w = sprite:getWidth()
|
||||
love.graphics.draw(sprite, math.floor((160 - w) / 2), y)
|
||||
local p = self.phase
|
||||
|
||||
if p == "show_player" then
|
||||
love.graphics.push()
|
||||
love.graphics.translate(-self.scx, 0)
|
||||
-- mon pic on the BG at hlcoord 7, 2; info box on the window at
|
||||
-- hWY $50, so it sits in the bottom half of the screen
|
||||
if self.monVisible and self.sentSprite then
|
||||
love.graphics.draw(self.sentSprite, 56, 16)
|
||||
end
|
||||
self:drawMonInfo(self.sent, self.playerOt, self.playerOtId, 10)
|
||||
love.graphics.pop()
|
||||
if self.poof and self.poof > 0 then
|
||||
love.graphics.setColor(0, 0, 0, self.poof / 16)
|
||||
love.graphics.circle("fill", 80, 40, 20 - (self.poof or 0))
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
elseif p == "open_cable" or p == "open_cable2" then
|
||||
love.graphics.push()
|
||||
love.graphics.translate(self.scx, 0)
|
||||
if self.img.openCable then
|
||||
love.graphics.draw(self.img.openCable, 64, 16) -- open end at SCX $f0
|
||||
end
|
||||
love.graphics.pop()
|
||||
|
||||
elseif p == "ball_enter" then
|
||||
if self.img.openCable then
|
||||
love.graphics.draw(self.img.openCable, 64, 16)
|
||||
end
|
||||
-- OAM coords carry a (+8, +16) hardware offset
|
||||
local ball = self.flash and (self.img.cableBallAlt or self.img.cableBall)
|
||||
or self.img.cableBall
|
||||
if ball then
|
||||
love.graphics.draw(ball, self.ballX - 8, self.ballY - 16)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.circle("fill", self.ballX, self.ballY - 8, 6)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
elseif p == "transfer_lr" or p == "transfer_rl" then
|
||||
-- two-screen world: left GB scene at x0, right GB scene at x160;
|
||||
-- the mon icon stays in screen space like the OAM sprite it ports
|
||||
love.graphics.push()
|
||||
love.graphics.translate(-self.scx, 0)
|
||||
self:drawLeftGB()
|
||||
love.graphics.translate(160, 0)
|
||||
self:drawRightGB()
|
||||
love.graphics.pop()
|
||||
local mon = (p == "transfer_lr") and self.sent or self.received
|
||||
self:drawIconInBubble(mon, self.monX, self.monY)
|
||||
if self.cableFlash then
|
||||
love.graphics.setColor(1, 1, 1, 0.15)
|
||||
love.graphics.rectangle("fill", 0, 32, 160, 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
elseif p == "show_enemy" then
|
||||
if self.monVisible and self.recvSprite then
|
||||
love.graphics.draw(self.recvSprite, 56, 16)
|
||||
end
|
||||
self:drawMonInfo(self.received, self.enemyName, self.enemyOtId, 10)
|
||||
end
|
||||
-- went_to / for_sends / farewell / delay: cleared window; TextBox draws
|
||||
end
|
||||
|
||||
return TradeAnim
|
||||
|
||||
@@ -34,7 +34,8 @@ function TrainerCard.new(game)
|
||||
local self = setmetatable({ game = game }, TrainerCard)
|
||||
local img = tryImage("assets/generated/trainer_card/badges.png")
|
||||
if img then
|
||||
-- 8 pairs of [gym leader face, badge]
|
||||
-- badges.2bpp is 8 stacked [face, badge] pairs (DrawBadges FaceBadgeTiles)
|
||||
self.faces = { img = img, quads = quads16(img, 8, 32, 0, 0) }
|
||||
self.badges = { img = img, quads = quads16(img, 8, 32, 0, 16) }
|
||||
end
|
||||
local nums = tryImage("assets/generated/trainer_card/badge_numbers.png")
|
||||
@@ -124,7 +125,7 @@ function TrainerCard:draw()
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
end
|
||||
|
||||
-- numbered badge grid (rows 11-17): earned solid, unearned dimmed
|
||||
-- numbered badge grid (rows 11-17): face by default, badge when owned
|
||||
self:frameBox(0, 11, 20, 7)
|
||||
local badges = Badges.list(self.game.data)
|
||||
for i = 1, #badges do
|
||||
@@ -136,12 +137,11 @@ function TrainerCard:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.nums.img, self.nums.quads[i - 1], tx, ty)
|
||||
end
|
||||
if self.badges and self.badges.quads[i - 1]
|
||||
and save.inventory[Badges.itemFor(badges[i])] then
|
||||
-- unearned badge slots stay blank (DrawBadges)
|
||||
if self.faces and self.faces.quads[i - 1] then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.badges.img, self.badges.quads[i - 1],
|
||||
tx + 4, ty + 6)
|
||||
local owned = save.inventory[Badges.itemFor(badges[i])]
|
||||
local sheet = owned and self.badges or self.faces
|
||||
love.graphics.draw(sheet.img, sheet.quads[i - 1], tx + 4, ty + 6)
|
||||
end
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
Reference in New Issue
Block a user