CLOSES #941, CLOSES #1081, CLOSES #1349, CLOSES #1602, CLOSES #1618, CLOSES #1624, CLOSES #1630, CLOSES #1633, CLOSES #1661, CLOSES #1669, CLOSES #1676, CLOSES #1731, CLOSES #1739, CLOSES #1740, CLOSES #1742, CLOSES #1744, CLOSES #1753, CLOSES #1757, CLOSES #1758, CLOSES #1760, CLOSES #1761, CLOSES #1762, CLOSES #1763, CLOSES #1765, CLOSES #1770, CLOSES #1777, CLOSES #1781, CLOSES #1783, CLOSES #1784, CLOSES #1785, CLOSES #1787, CLOSES #1788, CLOSES #1789, CLOSES #1790, CLOSES #1792, CLOSES #1797, CLOSES #1803

This commit is contained in:
bryanthaboi
2026-08-25 07:55:52 -04:00
parent ba201aa4ad
commit 1850b50ee6
50 changed files with 659 additions and 290 deletions
+21 -25
View File
@@ -42,27 +42,29 @@ local LINK_HEADINGS = { "HOW TO LINK", "COLOSSEUM", "TRADE CENTER" }
local function linkCableHelp(game) local function linkCableHelp(game)
local text = game.data.text or {} local text = game.data.text or {}
local items, showMenu, askHeading local items, menu, openMenu
function showMenu() local function closeAll()
game.stack:push(Menu.new(game, items, game.stack:pop()
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 1 }))
end
function askHeading()
game.stack:push(TextBox.new(game,
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
showMenu))
end end
items = {} items = {}
for i, label in ipairs(LINK_HEADINGS) do for i, label in ipairs(LINK_HEADINGS) do
items[i] = { label = label, onSelect = function() items[i] = { label = label, keepOpen = true, onSelect = function()
game.stack:push(TextBox.new(game, game.stack:push(TextBox.new(game,
text["_LinkCableInfoText" .. i] or label, askHeading)) text["_LinkCableInfoText" .. i] or label))
end } end }
end end
items[#items + 1] = { label = "STOP READING" } items[#items + 1] = { label = "STOP READING", onSelect = closeAll }
menu = Menu.new(game, items,
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 2, itemY = 2,
onCancel = closeAll })
function openMenu() game.stack:push(menu) end
game.stack:push(TextBox.new(game, game.stack:push(TextBox.new(game,
text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable", text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable",
askHeading)) function()
game.stack:push(TextBox.new(game,
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
nil, { stay = { onShown = openMenu } }))
end))
end end
return { return {
@@ -84,21 +86,15 @@ return {
{ "jump_if_false", 5 }, -- 2 { "jump_if_false", 5 }, -- 2
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE", { "hide_object", "CELADON_MANSION_ROOF_HOUSE",
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 3 (old saves) "CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 3 (old saves)
{ "jump", 13 }, -- 4 { "jump", 11 }, -- 4
{ "give_pokemon", "EEVEE", 25 }, -- 5 { "give_pokemon", "EEVEE", 25, false, true }, -- 5
{ "jump_if_false", 12 }, -- 6 (party+box full) { "jump_if_false", 10 }, -- 6 (party+box full)
-- flag + HideObject before the jingle and GotMonText: the nickname -- scripts/CeladonMansionRoofHouse.asm:17-20 (#426)
-- prompt inside give_pokemon and the text row both yield, and a
-- script that dies there would leave the EEVEE taken with the ball
-- still on the table and the gift claimable again (#426). The row
-- count is unchanged, so the numeric jump targets still hold.
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7 { "set_flag", "EVENT_GOT_EEVEE" }, -- 7
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE", { "hide_object", "CELADON_MANSION_ROOF_HOUSE",
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8 "CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle) { "jump", 11 }, -- 9
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10 { "show_text", "_BoxIsFullText" }, -- 10
{ "jump", 13 }, -- 11
{ "show_text", "_BoxIsFullText" }, -- 12
}, },
}, },
} }
+2 -4
View File
@@ -2,12 +2,10 @@
return { return {
CELADON_CITY = { CELADON_CITY = {
talk = { talk = {
-- CeladonCityPoliwrathText (scripts/CeladonCity.asm): text_far
-- _CeladonCityPoliwrathText, then plays the POLIWRATH cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONCITY_POLIWRATH = { TEXT_CELADONCITY_POLIWRATH = {
{"face_player"}, {"face_player"},
-- pokered/scripts/CeladonCity.asm:90
{"play_cry", "POLIWRATH", true},
{"show_text", "_CeladonCityPoliwrathText"}, {"show_text", "_CeladonCityPoliwrathText"},
}, },
}, },
+6 -12
View File
@@ -2,30 +2,24 @@
return { return {
CELADON_MANSION_1F = { CELADON_MANSION_1F = {
talk = { talk = {
-- CeladonMansion1FClefairyText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FClefairyText, then plays the CLEFAIRY cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_CLEFAIRY = { TEXT_CELADONMANSION1F_CLEFAIRY = {
{"face_player"}, {"face_player"},
-- pokered/scripts/CeladonMansion1F.asm:27
{"play_cry", "CLEFAIRY", true},
{"show_text", "_CeladonMansion1FClefairyText"}, {"show_text", "_CeladonMansion1FClefairyText"},
}, },
-- CeladonMansion1FMeowthText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FMeowthText, then plays the MEOWTH cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_MEOWTH = { TEXT_CELADONMANSION1F_MEOWTH = {
{"face_player"}, {"face_player"},
-- pokered/scripts/CeladonMansion1F.asm:18
{"play_cry", "MEOWTH", true},
{"show_text", "_CeladonMansion1FMeowthText"}, {"show_text", "_CeladonMansion1FMeowthText"},
}, },
-- CeladonMansion1FNidoranFText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FNidoranFText, then plays the NIDORAN_F cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_NIDORANF = { TEXT_CELADONMANSION1F_NIDORANF = {
{"face_player"}, {"face_player"},
-- pokered/scripts/CeladonMansion1F.asm:33
{"play_cry", "NIDORAN_F", true},
{"show_text", "_CeladonMansion1FNidoranFText"}, {"show_text", "_CeladonMansion1FNidoranFText"},
}, },
}, },
@@ -4,11 +4,10 @@
return { return {
LAVENDER_CUBONE_HOUSE = { LAVENDER_CUBONE_HOUSE = {
talk = { talk = {
-- LavenderCuboneHouseCuboneText: text_far _LavenderCuboneHouseCuboneText, -- LavenderCuboneHouse.asm:10
-- then text_asm plays the CUBONE cry. Cry playback has no Commands
-- equivalent in this port, so just show the line.
TEXT_LAVENDERCUBONEHOUSE_CUBONE = { TEXT_LAVENDERCUBONEHOUSE_CUBONE = {
{ "face_player" }, { "face_player" },
{ "play_cry", "CUBONE", true },
{ "show_text", "_LavenderCuboneHouseCuboneText" }, { "show_text", "_LavenderCuboneHouseCuboneText" },
}, },
+4 -5
View File
@@ -31,16 +31,15 @@ M.MR_FUJIS_HOUSE = {
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6 { "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
}, },
-- scripts/MrFujisHouse.asm MrFujisHousePsyduckText: text_far then -- scripts/MrFujisHouse.asm:56
-- PlayCry(PSYDUCK). Cry playback isn't modeled by Commands, so just
-- show the flavor text.
TEXT_MRFUJISHOUSE_PSYDUCK = { TEXT_MRFUJISHOUSE_PSYDUCK = {
{ "play_cry", "PSYDUCK", true },
{ "show_text", "_MrFujisHousePsyduckText" }, { "show_text", "_MrFujisHousePsyduckText" },
}, },
-- scripts/MrFujisHouse.asm MrFujisHouseNidorinoText: text_far then -- scripts/MrFujisHouse.asm:63
-- PlayCry(NIDORINO).
TEXT_MRFUJISHOUSE_NIDORINO = { TEXT_MRFUJISHOUSE_NIDORINO = {
{ "play_cry", "NIDORINO", true },
{ "show_text", "_MrFujisHouseNidorinoText" }, { "show_text", "_MrFujisHouseNidorinoText" },
}, },
}, },
+2 -3
View File
@@ -4,10 +4,9 @@
return { return {
ROUTE_16_FLY_HOUSE = { ROUTE_16_FLY_HOUSE = {
talk = { talk = {
-- Route16FlyHouseFearowText: text_asm just prints the one line and -- scripts/Route16FlyHouse.asm:45-51
-- plays the FEAROW cry (no cry-playback command exists in this
-- port's Commands vocabulary, so only the text is ported).
TEXT_ROUTE16FLYHOUSE_FEAROW = { TEXT_ROUTE16FLYHOUSE_FEAROW = {
{ "play_cry", "FEAROW", true },
{ "show_text", "_Route16FlyHouseFearowText" }, { "show_text", "_Route16FlyHouseFearowText" },
}, },
}, },
+1
View File
@@ -1047,6 +1047,7 @@ local championsRoomRivalScript = {
-- numeric 26 into a jump ONTO the closing HALL_OF_FAME warp instead of past -- numeric 26 into a jump ONTO the closing HALL_OF_FAME warp instead of past
-- it, so a returning champion warped straight into the induction. -- it, so a returning champion warped straight into the induction.
{ "jump_if_true", "end" }, -- 3 { "jump_if_true", "end" }, -- 3
{ "set_option", "animations", true }, -- scripts/ChampionsRoom.asm:57
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4 { "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after -- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
-- the intro text, before the battle itself (#706); pushBattle's wipe-time -- the intro text, before the battle itself (#706); pushBattle's wipe-time
+9 -5
View File
@@ -359,18 +359,22 @@ M.ROUTE_16_FLY_HOUSE = {
TEXT_ROUTE16FLYHOUSE_BRUNETTE_GIRL = function(game, ow, npc, done) TEXT_ROUTE16FLYHOUSE_BRUNETTE_GIRL = function(game, ow, npc, done)
local t = text(game) local t = text(game)
if game.save.flags.EVENT_GOT_HM02 then if game.save.flags.EVENT_GOT_HM02 then
push(game, t._Route16FlyHouseBrunetteGirlHm02ExplanationText push(game, t._Route16FlyHouseBrunetteGirlHM02ExplanationText
or "HM02 is FLY!\fIt will whisk you\nback to any town!", done) or "HM02 is FLY.\nIt will take you\vback to any town.\fPut it to good\nuse!", done)
return return
end end
push(game, t._Route16FlyHouseBrunetteGirlText push(game, t._Route16FlyHouseBrunetteGirlText
or "Shh! It's a\nsecret!\fMy POKéMON's\nHM02, take it!", function() or "Oh, you found my\nsecret retreat!", function()
if not require("src.inventory.Bag").add(game.save, "HM_FLY", 1) then if not require("src.inventory.Bag").add(game.save, "HM_FLY", 1) then
push(game, "You don't have\nroom for HM02!", done) push(game, t._Route16FlyHouseBrunetteGirlHM02NoRoomText
or "You don't have any\nroom for this.", done)
return return
end end
game.save.flags.EVENT_GOT_HM02 = true game.save.flags.EVENT_GOT_HM02 = true
push(game, ("%s got\nHM02!"):format(game.save.player.name), done) -- scripts/Route16FlyHouse.asm:32-35
push(game, fill(t._Route16FlyHouseBrunetteGirlReceivedHM02Text
or "{PLAYER} received\nHM02!", { player = game.save.player.name }),
done, require("src.render.TextBox").soundOpts(game, "Get_Key_Item"))
end) end)
end, end,
}, },
+34 -6
View File
@@ -1170,6 +1170,17 @@ function love.run()
-- per-frame sleep-granularity jitter. -- per-frame sleep-granularity jitter.
local nextFrame = love.timer and love.timer.getTime() or 0 local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0 local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function() return function()
-- process events -- process events
@@ -1188,17 +1199,34 @@ function love.run()
return a or 0 return a or 0
end end
end end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
love.handlers[name](a, b, c, d, e, f) love.handlers[name](a, b, c, d, e, f)
end end
end end
-- update dt -- update dt
if love.timer then dt = love.timer.step() end if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
-- call update and draw -- call update and draw
if love.update then love.update(dt) end if love.update then love.update(dt) end
if love.graphics and love.graphics.isActive() then local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin() love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor()) love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end if love.draw then love.draw() end
@@ -1209,9 +1237,9 @@ function love.run()
if paced then if paced then
-- Sleep out the remainder of the frame budget, measured from the -- Sleep out the remainder of the frame budget, measured from the
-- carried deadline, in small chunks so the OS timer stays -- carried deadline, in small chunks so the OS timer stays
-- responsive. vsync is untouched: when it already paces slower -- responsive. The pacer yields to vsync inside a 1ms dead band, so
-- than the cap the remainder is <= 0 and this rounds to a no-op. -- when the panel already paces at or below the cap it is a no-op.
local budget = 1 / FrameCap.current local budget = 1 / cap
nextFrame = nextFrame + budget nextFrame = nextFrame + budget
local now = love.timer.getTime() local now = love.timer.getTime()
-- A stall (alt-tab, a GC pause, a blocked import) can leave the -- A stall (alt-tab, a GC pause, a blocked import) can leave the
@@ -1222,8 +1250,8 @@ function love.run()
end end
while true do while true do
local remaining = nextFrame - love.timer.getTime() local remaining = nextFrame - love.timer.getTime()
if remaining <= 0 then break end if remaining <= SLEEP_FLOOR then break end
love.timer.sleep(remaining < 0.001 and remaining or 0.001) love.timer.sleep(0.001)
end end
else else
love.timer.sleep(0.001) love.timer.sleep(0.001)
+59 -10
View File
@@ -97,7 +97,7 @@ end
-- space. -- space.
function BattleState:extendedHUD() function BattleState:extendedHUD()
local options = self.game and self.game.save and self.game.save.options local options = self.game and self.game.save and self.game.save.options
local bg = options and options.battleBg local bg = options and self:bgMode()
return self:wideLayout() return self:wideLayout()
and options and options.battleHud == "extended" and options and options.battleHud == "extended"
and ((options.battleFit == "fixed" and ((options.battleFit == "fixed"
@@ -108,12 +108,12 @@ end
function BattleState:extendedWorldHUD() function BattleState:extendedWorldHUD()
local options = self.game and self.game.save and self.game.save.options local options = self.game and self.game.save and self.game.save.options
return self:extendedHUD() and options return self:extendedHUD() and options
and options.battleFit == "fixed" and options.battleBg == "world" and options.battleFit == "fixed" and self:bgMode() == "world"
end end
function BattleState:extendedBlackHUD() function BattleState:extendedBlackHUD()
local options = self.game and self.game.save and self.game.save.options local options = self.game and self.game.save and self.game.save.options
return self:extendedHUD() and options and options.battleBg == "black" return self:extendedHUD() and options and self:bgMode() == "black"
end end
-- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids -- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids
@@ -130,6 +130,10 @@ end
-- opaque 160x144 field over the top, so only the surround changes. -- opaque 160x144 field over the top, so only the surround changes.
function BattleState:bgMode() function BattleState:bgMode()
local options = self.game and self.game.save and self.game.save.options local options = self.game and self.game.save and self.game.save.options
if options and self:wideLayout() and options.battleFit == "fill"
and options.battleHud == "extended" then
return "white"
end
local mode = options and options.battleBg local mode = options and options.battleBg
if mode == "black" or mode == "world" then return mode end if mode == "black" or mode == "world" then return mode end
return "white" return "white"
@@ -212,6 +216,13 @@ local BALL_ANIMS = {
SHAKE_ANIM = true, SHOWPIC_ANIM = true, SHAKE_ANIM = true, SHOWPIC_ANIM = true,
} }
-- engine/battle/effects.asm:552
local ENEMY_STAT_DOWN_MISS = {
ATTACK_DOWN1_EFFECT = true, DEFENSE_DOWN1_EFFECT = true,
DEFENSE_DOWN2_EFFECT = true, SPEED_DOWN1_EFFECT = true,
ACCURACY_DOWN1_EFFECT = true,
}
local imageCache = {} local imageCache = {}
-- The three tables below are keyed by the Image OBJECT, not by a path, and a -- The three tables below are keyed by the Image OBJECT, not by a path, and a
-- running battle holds the pics it built at enter() (battler.sprite, -- running battle holds the pics it built at enter() (battler.sprite,
@@ -229,6 +240,11 @@ local imagePadLeft = setmetatable({}, WEAK_KEYS)
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be -- image -> { path, pal } so palette-fade variants (see fadeImage) can be
-- rebuilt for any battle pic, whatever code loaded it -- rebuilt for any battle pic, whatever code loaded it
local imageMeta = setmetatable({}, WEAK_KEYS) local imageMeta = setmetatable({}, WEAK_KEYS)
local function mattedPic(path)
return path:sub(1, 17) == "assets/generated/"
or path:sub(1, 17) == "save/mod-derived/"
end
-- pal = { name, colors } recolors the 4 GB shades like the Super Game Boy. -- pal = { name, colors } recolors the 4 GB shades like the Super Game Boy.
-- trueColor art (14 §the 4-shade contract) opts out of the quantize -- trueColor art (14 §the 4-shade contract) opts out of the quantize
-- entirely, so its palette variant collapses back onto the plain path. -- entirely, so its palette variant collapses back onto the plain path.
@@ -254,6 +270,7 @@ local function getImage(path, pal, trueColor)
end) end)
end end
local w, h = id:getDimensions() local w, h = id:getDimensions()
if mattedPic(Assets.resolve(path)) then
local bottom = h - 1 local bottom = h - 1
while bottom >= 0 do while bottom >= 0 do
local opaque = false local opaque = false
@@ -274,9 +291,10 @@ local function getImage(path, pal, trueColor)
if opaque then break end if opaque then break end
left = left + 1 left = left + 1
end end
img = love.graphics.newImage(id)
pad = h - 1 - bottom pad = h - 1 - bottom
padL = left padL = left
end
img = love.graphics.newImage(id)
else else
img = Assets.image(path) -- headless stub: no pixel access img = Assets.image(path) -- headless stub: no pixel access
end end
@@ -2214,6 +2232,9 @@ function BattleState:update(dt)
self.waitFrames = nil self.waitFrames = nil
if destination == "menu" then if destination == "menu" then
self.introSlide = nil self.introSlide = nil
-- engine/battle/core.asm:2007
self.msgHold = nil
self.shown = nil
self.phase = "menu" self.phase = "menu"
elseif destination == "finish" then elseif destination == "finish" then
self:finish() self:finish()
@@ -3041,18 +3062,33 @@ local BGP_INVERT = { [0] = 3, 2, 1, 0 } -- $1b (flash phase 1)
local BGP_WHITE = { [0] = 0, 0, 0, 0 } -- $00 (flash phase 2) local BGP_WHITE = { [0] = 0, 0, 0, 0 } -- $00 (flash phase 2)
local BGP_DARK = { [0] = 3, 3, 2, 1 } -- $6f DarkScreenPalette local BGP_DARK = { [0] = 3, 3, 2, 1 } -- $6f DarkScreenPalette
local BGP_LIGHT = { [0] = 0, 0, 1, 2 } -- $90 LightScreenPalette local BGP_LIGHT = { [0] = 0, 0, 1, 2 } -- $90 LightScreenPalette
local BGP_DARKEN = { [0] = 0, 1, 3, 3 } -- $f4 DarkenMonPalette (SGB) local BGP_DARKEN_SGB = { [0] = 0, 1, 3, 3 } -- $f4
local BGP_DARKEN_MONO = { [0] = 1, 2, 3, 3 } -- $f9
-- engine/battle/animations.asm:1090
local function onSgb()
local m = require("src.render.PaletteFX").mode
return m == "gbc" or m == "gbc_inv"
end
-- FlashScreenLongSGB (animations.asm:1010): 12 BGP values per cycle, -- FlashScreenLongSGB (animations.asm:1010): 12 BGP values per cycle,
-- 3 cycles; the first cycle holds each for 2 frames, the rest for 1 -- 3 cycles; the first cycle holds each for 2 frames, the rest for 1
-- (FlashScreenLongDelay) -- (FlashScreenLongDelay)
local FLASH_LONG_MAPS = { local FLASH_LONG_SGB = {
{ [0] = 0, 2, 3, 3 }, { [0] = 0, 3, 3, 3 }, { [0] = 3, 3, 3, 3 }, { [0] = 0, 2, 3, 3 }, { [0] = 0, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
{ [0] = 0, 3, 3, 3 }, { [0] = 0, 2, 3, 3 }, { [0] = 0, 1, 2, 3 }, { [0] = 0, 3, 3, 3 }, { [0] = 0, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
} }
-- engine/battle/animations.asm:992
local FLASH_LONG_MONO = {
{ [0] = 1, 2, 3, 3 }, { [0] = 2, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
{ [0] = 2, 3, 3, 3 }, { [0] = 1, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
}
-- the shade map in force this frame (a running flash wins over the -- the shade map in force this frame (a running flash wins over the
-- persistent palette) -- persistent palette)
function BattleState:activeBgp() function BattleState:activeBgp()
@@ -3186,7 +3222,7 @@ function BattleState:applyAnimEffect(ev)
elseif e == "SE_LIGHT_SCREEN_PALETTE" then elseif e == "SE_LIGHT_SCREEN_PALETTE" then
fx.bgp = BGP_LIGHT fx.bgp = BGP_LIGHT
elseif e == "SE_DARKEN_MON_PALETTE" then elseif e == "SE_DARKEN_MON_PALETTE" then
fx.bgp = BGP_DARKEN fx.bgp = onSgb() and BGP_DARKEN_SGB or BGP_DARKEN_MONO
elseif e == "SE_RESET_SCREEN_PALETTE" then elseif e == "SE_RESET_SCREEN_PALETTE" then
fx.bgp = nil fx.bgp = nil
elseif e == "SE_DARK_SCREEN_FLASH" then elseif e == "SE_DARK_SCREEN_FLASH" then
@@ -3196,8 +3232,9 @@ function BattleState:applyAnimEffect(ev)
idx = 1, left = 2 } idx = 1, left = 2 }
elseif e == "SE_FLASH_SCREEN_LONG" then elseif e == "SE_FLASH_SCREEN_LONG" then
local steps = {} local steps = {}
local maps = onSgb() and FLASH_LONG_SGB or FLASH_LONG_MONO
for cycle = 1, 3 do for cycle = 1, 3 do
for _, m in ipairs(FLASH_LONG_MAPS) do for _, m in ipairs(maps) do
steps[#steps + 1] = { map = m, frames = (cycle == 1) and 2 or 1 } steps[#steps + 1] = { map = m, frames = (cycle == 1) and 2 or 1 }
end end
end end
@@ -4139,6 +4176,12 @@ function BattleState:performMove(user, target, moveInst, isCalled)
-- pure status moves -- pure status moves
if move.power == 0 and record and record.kind == "primary" and record.run then if move.power == 0 and record and record.kind == "primary" and record.run then
if ENEMY_STAT_DOWN_MISS[move.effect] and not user.isPlayer
and self.kind ~= "link" and self.rng(0, 255) < 64 then
self:cancelMoveAnim()
self:sayNext(self:romText("_AttackMissedText", "%s's\nattack missed!", displayName(user)))
return
end
-- accuracy-checked status effects run MoveHitTest, which has no -- accuracy-checked status effects run MoveHitTest, which has no
-- 100%-accuracy early-out (even Thunder Wave misses on the 255 -- 100%-accuracy early-out (even Thunder Wave misses on the 255
-- roll) and misses outright against a mid-Fly/Dig target; the -- roll) and misses outright against a mid-Fly/Dig target; the
@@ -4417,7 +4460,11 @@ function BattleState:awardExp()
-- new current HP (house convention: potions drain the bar too, see -- new current HP (house convention: potions drain the bar too, see
-- itemUsed) so the bar grows instead. Only the active player battler -- itemUsed) so the bar grows instead. Only the active player battler
-- shares its table with the HUD; other party mons (EXP.ALL) have no bar. -- shares its table with the HUD; other party mons (EXP.ALL) have no bar.
if mon == self.player.mon then self:drainNext() end if mon == self.player.mon then
-- engine/battle/experience.asm:236
self.player.badgeExtraBoosts = nil
self:drainNext()
end
for _, moveId in ipairs(Experience.movesLearnedAt( for _, moveId in ipairs(Experience.movesLearnedAt(
self.data.pokemon[mon.species], lv)) do self.data.pokemon[mon.species], lv)) do
self:learnMove(mon, moveId) self:learnMove(mon, moveId)
@@ -5920,7 +5967,9 @@ function BattleState:animSpriteColors(s, px, py)
local P local P
-- engine/battle/animations.asm:551 (.notSGB) -- engine/battle/animations.asm:551 (.notSGB)
if PaletteFX.usesSpriteObp() then if PaletteFX.usesSpriteObp() then
P = PaletteFX.ogObj() -- engine/battle/init_battle_variables.asm:18
P = require("src.core.GameVersion").isBlue() and PaletteFX.GBC_OBJ_BLUE
or PaletteFX.GBC_OBJ
if key == "f0" then key = "e4" elseif key == "f0x" then key = "e4x" end if key == "f0" then key = "e4" elseif key == "f0x" then key = "e4x" end
else else
P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4)) P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4))
+30 -8
View File
@@ -41,6 +41,34 @@ local function badgeBoost(battler, stat)
return nil return nil
end end
-- engine/battle/core.asm:6454
function Damage.applyBadgeBoost(battler, stat, value)
if battler.hazeStatReset then return value end
local row = badgeBoost(battler, stat)
if not row then return value end
local extra = battler.badgeExtraBoosts and battler.badgeExtraBoosts[stat] or 0
for _ = 1, 1 + extra do
value = math.min(999, math.floor(value * (row.num or 9) / (row.den or 8)))
end
return value
end
-- engine/battle/effects.asm:498,689
function Damage.reapplyBadgeBoosts(battler, changedStat)
if not battler or not battler.badges then return end
local extra = battler.badgeExtraBoosts
if not extra then extra = {} battler.badgeExtraBoosts = extra end
for _, row in ipairs(battler.badgeBoosts or Damage.BADGE_BOOSTS) do
if battler.badges[row.badge] then
if row.stat == changedStat then
extra[row.stat] = 0
else
extra[row.stat] = (extra[row.stat] or 0) + 1
end
end
end
end
-- the merged status record for a battler's persistent condition, or nil -- the merged status record for a battler's persistent condition, or nil
local function statusRecord(battler) local function statusRecord(battler)
return Status.recordFor(battler.statuses, battler.mon.status) return Status.recordFor(battler.statuses, battler.mon.status)
@@ -178,14 +206,8 @@ function Damage.compute(ruleset, attacker, defender, move, opts)
-- badge boosts (x9/8), engine/battle/core.asm ApplyBadgeStatBoosts: -- badge boosts (x9/8), engine/battle/core.asm ApplyBadgeStatBoosts:
-- Boulder -> attack, Thunder -> defense, Soul -> speed (TurnOrder), -- Boulder -> attack, Thunder -> defense, Soul -> speed (TurnOrder),
-- Volcano -> special -- Volcano -> special
local atkBoost = badgeBoost(attacker, atkStat) atk = Damage.applyBadgeBoost(attacker, atkStat, atk)
if atkBoost then dfn = Damage.applyBadgeBoost(defender, defStat, dfn)
atk = math.floor(atk * (atkBoost.num or 9) / (atkBoost.den or 8))
end
local defBoost = badgeBoost(defender, defStat)
if defBoost then
dfn = math.floor(dfn * (defBoost.num or 9) / (defBoost.den or 8))
end
-- burn halves physical attack (applied as part of the stat in Gen 1; -- burn halves physical attack (applied as part of the stat in Gen 1;
-- the status record's statPenalty names the stat it cuts). -- the status record's statPenalty names the stat it cuts).
-- hazeStatReset suppresses it: Haze (haze.asm ResetStats) copied the -- hazeStatReset suppresses it: Haze (haze.asm ResetStats) copied the
+11
View File
@@ -11,6 +11,7 @@
-- is the registry view of all three -- the merged Data.move_effects a -- is the registry view of all three -- the merged Data.move_effects a
-- battle dispatches on serves these same objects. -- battle dispatches on serves these same objects.
local Damage = require("src.battle.Damage")
local Logger = require("src.core.Logger") local Logger = require("src.core.Logger")
local StatusRegistry = require("src.battle.StatusRegistry") local StatusRegistry = require("src.battle.StatusRegistry")
local TurnOrder = require("src.battle.TurnOrder") local TurnOrder = require("src.battle.TurnOrder")
@@ -57,6 +58,10 @@ local function changeStage(battle, who, stat, delta, fromEnemy)
-- recomputed and QuarterSpeedDueToParalysis/HalveAttackDueToBurn re-run, -- recomputed and QuarterSpeedDueToParalysis/HalveAttackDueToBurn re-run,
-- re-baking the burn/para penalty and ending Haze's temporary lift. -- re-baking the burn/para penalty and ending Haze's temporary lift.
who.hazeStatReset = nil who.hazeStatReset = nil
if battle.ruleset and battle.ruleset.badgeBoostReapplyBug
and battle.kind ~= "link" and who == battle.player then
Damage.reapplyBadgeBoosts(who, stat)
end
-- _MonsStatsRoseText/_MonsStatsFellText: "X's / STAT rose!"; the -- _MonsStatsRoseText/_MonsStatsFellText: "X's / STAT rose!"; the
-- two-stage variants scroll "greatly" onto a third line -- two-stage variants scroll "greatly" onto a third line
local label = Strings(STAT_LABEL[stat]) -- looked up here, not at require (#811) local label = Strings(STAT_LABEL[stat]) -- looked up here, not at require (#811)
@@ -132,6 +137,11 @@ end
local function statDownSide(stat) local function statDownSide(stat)
return function(battle, user, target) return function(battle, user, target)
-- engine/battle/effects.asm:552
if not user.isPlayer and battle.kind ~= "link"
and battle.rng(0, 255) < 64 then
return {}
end
if target.substituteHP then return {} end if target.substituteHP then return {} end
if battle.rng(0, 255) >= 85 then return {} end -- 33 percent + 1 (85/256) if battle.rng(0, 255) >= 85 then return {} end -- 33 percent + 1 (85/256)
-- StatModifierDownEffect's side-effect branch never runs MoveHitTest, -- StatModifierDownEffect's side-effect branch never runs MoveHitTest,
@@ -254,6 +264,7 @@ MoveEffects.primary = {
-- Attack-halving and paralysis Speed-quartering on BOTH battlers -- Attack-halving and paralysis Speed-quartering on BOTH battlers
-- until the next stat recompute (a stage change or switch-in). -- until the next stat recompute (a stage change or switch-in).
b.hazeStatReset = true b.hazeStatReset = true
b.badgeExtraBoosts = nil
end end
-- Gen 1 also removes the enemy's major status; if that cured sleep -- Gen 1 also removes the enemy's major status; if that cured sleep
-- or freeze, the target forfeits its move this turn (haze.asm -- or freeze, the target forfeits its move this turn (haze.asm
+1 -9
View File
@@ -14,15 +14,7 @@ local function effectiveSpeed(battler)
battler.stages and battler.stages.speed or 0) battler.stages and battler.stages.speed or 0)
-- ApplyBadgeStatBoosts: the SOULBADGE boosts speed; the rows come from -- ApplyBadgeStatBoosts: the SOULBADGE boosts speed; the rows come from
-- the battler's merged badgeBoosts with the vanilla list as fallback -- the battler's merged badgeBoosts with the vanilla list as fallback
local badges = battler.badges spd = Damage.applyBadgeBoost(battler, "speed", spd)
if badges then
for _, row in ipairs(battler.badgeBoosts or Damage.BADGE_BOOSTS) do
if row.stat == "speed" and badges[row.badge] then
spd = math.floor(spd * (row.num or 9) / (row.den or 8))
break
end
end
end
-- paralysis quarters speed (the status record's statPenalty); -- paralysis quarters speed (the status record's statPenalty);
-- hazeStatReset suppresses it because Haze (haze.asm ResetStats) -- hazeStatReset suppresses it because Haze (haze.asm ResetStats)
-- copied the unmodified speed over the quartered battle stat, lifting -- copied the unmodified speed over the quartered battle stat, lifting
+14 -1
View File
@@ -97,6 +97,19 @@ local function battleIsTopState(battle)
return not (stack and stack.top) or stack:top() == battle return not (stack and stack.top) or stack:top() == battle
end end
-- engine/menus/party_menu.asm:4
local function coveredByOpaqueState(battle)
local stack = battle.game and battle.game.stack
local states = stack and stack.states
if not states then return false end
local above = false
for i = 1, #states do
if above and states[i] and states[i].isOpaque then return true end
if states[i] == battle then above = true end
end
return false
end
local function anchorHUD(battle, x, y, w, h, anchor) local function anchorHUD(battle, x, y, w, h, anchor)
if not battle:extendedHUD() or not battleIsTopState(battle) then return end if not battle:extendedHUD() or not battleIsTopState(battle) then return end
local renderer = battle.game and battle.game.renderer local renderer = battle.game and battle.game.renderer
@@ -351,7 +364,7 @@ function WideBattle.draw(battle)
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT) g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
end end
-- AskName clears the field the same way the classic layout does -- AskName clears the field the same way the classic layout does
if battle.blankForAskName then return end if battle.blankForAskName or coveredByOpaqueState(battle) then return end
local fx = battle.fx local fx = battle.fx
local sx = (fx and fx.shakeX) or 0 local sx = (fx and fx.shakeX) or 0
+64 -7
View File
@@ -645,6 +645,7 @@ function Battle:smartAiState()
-- wPlayerSubStatus5 & SUBSTATUS_LOCK_ON: the enemy's OWN Lock-On, since -- wPlayerSubStatus5 & SUBSTATUS_LOCK_ON: the enemy's OWN Lock-On, since
-- BattleCommand_LockOn sets the bit on the target it was aimed at. -- BattleCommand_LockOn sets the bit on the target it was aimed at.
playerLockOn = playerState.lockOn or nil, playerLockOn = playerState.lockOn or nil,
playerIdentified = playerState.identified or nil,
playerPhysicalMoves = physical, playerPhysicalMoves = physical,
enemyRage = enemyState.rage, enemyRage = enemyState.rage,
enemyRageCount = enemyState.rageCount, enemyRageCount = enemyState.rageCount,
@@ -1082,7 +1083,7 @@ function Battle:hitOnce(attacker, defender, def, opts)
local attackerStages = self.stages[self:sideOf(attacker)] local attackerStages = self.stages[self:sideOf(attacker)]
local defenderStages = self.stages[self:sideOf(defender)] local defenderStages = self.stages[self:sideOf(defender)]
local types = self.data.type_chart and self.data.type_chart.types local types = self.data.type_chart and self.data.type_chart.types
local matchups = self.data.type_chart and self.data.type_chart.matchups local matchups = self:matchupsAgainst(defender)
local heldEffect, heldParam = self:heldEffect(attacker, "damage") local heldEffect, heldParam = self:heldEffect(attacker, "damage")
-- BattleCommand_Critical: SUBSTATUS_FOCUS_ENERGY (Focus Energy or a -- BattleCommand_Critical: SUBSTATUS_FOCUS_ENERGY (Focus Energy or a
@@ -1202,6 +1203,8 @@ function Battle:hitOnce(attacker, defender, def, opts)
if def.effect == "EFFECT_FALSE_SWIPE" and damage >= (defender.hp or 0) then if def.effect == "EFFECT_FALSE_SWIPE" and damage >= (defender.hp or 0) then
damage = math.max(0, (defender.hp or 0) - 1) damage = math.max(0, (defender.hp or 0) - 1)
end end
-- engine/battle_anims/anim_commands.asm:1200
if self.moveEvent then self.moveEvent.effectiveness = info.effectiveness end
return self:dealDamage(attacker, defender, damage, { return self:dealDamage(attacker, defender, damage, {
critical = critical, effectiveness = info.effectiveness, critical = critical, effectiveness = info.effectiveness,
-- Counter answers physical damage and Mirror Coat special, so what kind -- Counter answers physical damage and Mirror Coat special, so what kind
@@ -1690,6 +1693,13 @@ function Battle:useMove(attacker, defender, moveId)
text = ("Magnitude %d!"):format(number) }) text = ("Magnitude %d!"):format(number) })
end end
-- data/moves/effects.asm:1607, :1649
if def.effect == "EFFECT_RETURN" then
powerOverride = Effects.happinessPower(attacker.happiness)
elseif def.effect == "EFFECT_FRUSTRATION" then
powerOverride = Effects.happinessPower(attacker.happiness, true)
end
if not sureHit if not sureHit
and not self:accuracyRoll(def, attacker, defender) then and not self:accuracyRoll(def, attacker, defender) then
-- data/moves/effects.asm:148-151: `selfdestruct` sits between checkhit and -- data/moves/effects.asm:148-151: `selfdestruct` sits between checkhit and
@@ -1744,7 +1754,7 @@ function Battle:useMove(attacker, defender, moveId)
-- thing that stops SONIC BOOM, NIGHT SHADE or SUPER FANG. -- thing that stops SONIC BOOM, NIGHT SHADE or SUPER FANG.
local defenderTypes = (self:speciesDef(defender) or {}).types local defenderTypes = (self:speciesDef(defender) or {}).types
or defender.types or defender.types
local matchups = self.data.type_chart and self.data.type_chart.matchups local matchups = self:matchupsAgainst(defender)
if Damage.typeMultiplier(def.type, defenderTypes, matchups) == 0 then if Damage.typeMultiplier(def.type, defenderTypes, matchups) == 0 then
self:markMissed() self:markMissed()
self:emit({ kind = "message", self:emit({ kind = "message",
@@ -2119,6 +2129,21 @@ Battle.MOVE_EFFECTS.EFFECT_LOCK_ON = function(self, attacker, defender)
text = self:monName(attacker) .. " took aim!" }) text = self:monName(attacker) .. " took aim!" })
end end
-- engine/battle/move_effects/foresight.asm
Battle.MOVE_EFFECTS.EFFECT_FORESIGHT = function(self, attacker, defender,
def, _, sureHit)
if not sureHit
and not self:accuracyRoll(def, attacker, defender) then
return fail(self)
end
local target = self:volatile(defender)
if target.vanished or target.identified then return fail(self) end
target.identified = true
self:emit({ kind = "message",
text = self:monName(attacker) .. " identified "
.. self:monName(defender) .. "!" })
end
-- BattleCommand_CheckHit's .LockOn: the flag is read AND cleared by the next -- BattleCommand_CheckHit's .LockOn: the flag is read AND cleared by the next
-- move aimed at the mon carrying it, whether or not that move was the one the -- move aimed at the mon carrying it, whether or not that move was the one the
-- lock-on was meant for, and whether or not the exception at :1683-1688 then -- lock-on was meant for, and whether or not the exception at :1683-1688 then
@@ -2166,9 +2191,15 @@ function Battle:accuracyRoll(def, attacker, defender, accuracy)
end end
function Battle:vanillaAccuracyRoll(accuracy, attacker, defender) function Battle:vanillaAccuracyRoll(accuracy, attacker, defender)
return Damage.rollHit(self:moveAccuracy(accuracy, defender), local acc = self.stages[self:sideOf(attacker)].accuracy
self.stages[self:sideOf(attacker)].accuracy, local eva = self.stages[self:sideOf(defender)].evasion
self.stages[self:sideOf(defender)].evasion, self.random) -- engine/battle/effect_commands.asm:1786
if defender and self:volatile(defender).identified
and (eva or 0) >= (acc or 0) then
acc, eva = 0, 0
end
return Damage.rollHit(self:moveAccuracy(accuracy, defender), acc, eva,
self.random)
end end
-- BattleCommand_StatDown's SUBSTATUS_MIST arm (a GUARD SPEC): a drop the FOE -- BattleCommand_StatDown's SUBSTATUS_MIST arm (a GUARD SPEC): a drop the FOE
@@ -2354,7 +2385,7 @@ Battle.MOVE_EFFECTS.EFFECT_FUTURE_SIGHT = function(self, attacker, defender, def
stages = self.stages[self:sideOf(defender)], stages = self.stages[self:sideOf(defender)],
}, },
types = self.data.type_chart and self.data.type_chart.types, types = self.data.type_chart and self.data.type_chart.types,
matchups = self.data.type_chart and self.data.type_chart.matchups, matchups = self:matchupsAgainst(defender),
random = self.random, random = self.random,
}) })
state.futureSight = Effects.FUTURE_SIGHT_TURNS state.futureSight = Effects.FUTURE_SIGHT_TURNS
@@ -3072,6 +3103,30 @@ function Battle:safeguarded(mon)
return (self.screens[self:sideOf(mon)].safeguard or 0) > 0 return (self.screens[self:sideOf(mon)].safeguard or 0) > 0
end end
-- engine/battle/effect_commands.asm:1305
function Battle:matchupsAgainst(defender)
local chart = self.data.type_chart
local rows = chart and chart.matchups
if not rows or not defender then return rows end
if not self:volatile(defender).identified then return rows end
local skipped = chart.foresightMatchups
if not skipped or #skipped == 0 then return rows end
if not self.identifiedMatchups then
local drop = {}
for _, row in ipairs(skipped) do
drop[tostring(row.attacker) .. "/" .. tostring(row.defender)] = true
end
local out = {}
for _, row in ipairs(rows) do
if not drop[tostring(row.attacker) .. "/" .. tostring(row.defender)] then
out[#out + 1] = row
end
end
self.identifiedMatchups = out
end
return self.identifiedMatchups
end
-- `source` is the battler that inflicted it, carried only so -- `source` is the battler that inflicted it, carried only so
-- battle.status_inflicted can name it the way Gen 1's does. -- battle.status_inflicted can name it the way Gen 1's does.
-- BattleCommand_Paralyze and BattleCommand_Poison refuse on a zero matchup, -- BattleCommand_Paralyze and BattleCommand_Poison refuse on a zero matchup,
@@ -3084,7 +3139,7 @@ function Battle:statusRefusedByType(defender, moveType, status)
end end
local types = (self:speciesDef(defender) or {}).types or defender.types or {} local types = (self:speciesDef(defender) or {}).types or defender.types or {}
if moveType then if moveType then
local matchups = self.data.type_chart and self.data.type_chart.matchups local matchups = self:matchupsAgainst(defender)
if Damage.typeMultiplier(moveType, types, matchups) == 0 then return true end if Damage.typeMultiplier(moveType, types, matchups) == 0 then return true end
end end
if status == "poison" or status == "toxic" then if status == "poison" or status == "toxic" then
@@ -3917,6 +3972,8 @@ end
-- check. Split out because playerAttack needs the same answer. -- check. Split out because playerAttack needs the same answer.
function Battle:lockedInMove(mon) function Battle:lockedInMove(mon)
local state = self:volatile(mon) local state = self:volatile(mon)
-- engine/battle/core.asm:543
if state.chargeMove then return state.chargeMove end
if state.rolloutLock then return state.rolloutLock end if state.rolloutLock then return state.rolloutLock end
if state.rampageMove and (state.rampageTurns or 0) > 0 then if state.rampageMove and (state.rampageTurns or 0) > 0 then
return state.rampageMove return state.rampageMove
+8
View File
@@ -284,6 +284,14 @@ function Effects.magnitudePower(random)
return last[2], last[3] return last[2], last[3]
end end
-- engine/battle/move_effects/return.asm:1-24, frustration.asm:1-25
function Effects.happinessPower(happiness, frustration)
local h = happiness or 0
if h < 0 then h = 0 elseif h > 255 then h = 255 end
if frustration then h = 255 - h end
return math.floor(h * 10 / 25)
end
-- ------------------------------------------------------------------- weather -- ------------------------------------------------------------------- weather
-- --
-- BattleCommand_StartRain / StartSun / StartSandstorm all set wWeatherCount to -- BattleCommand_StartRain / StartSun / StartSandstorm all set wWeatherCount to
+5 -2
View File
@@ -58,9 +58,12 @@ Prize.AMULET_COIN = "AMULET_COIN"
-- data/text/battle.asm. Declared here and formatted at the call site so -- data/text/battle.asm. Declared here and formatted at the call site so
-- Strings.source is what registers them, the same way Decorations declares -- Strings.source is what registers them, the same way Decorations declares
-- its own five. No line markers: every battle message in this port is one -- its own five. No line markers: every battle message in this port is one
-- flowing string that Chrome.wrap breaks to the box. -- flowing string that Chrome.wrap breaks to the box, except SentSomeToMomText,
-- which keeps the cart's own `line`/`cont` breaks because it does not fit two
-- rows.
local GOT_MONEY = Strings.source("%s got %s%d for winning!") local GOT_MONEY = Strings.source("%s got %s%d for winning!")
local SENT_SOME = Strings.source("%s got %s%d for winning! Sent some to MOM!") -- data/text/battle.asm:179-185
local SENT_SOME = Strings.source("%s got %s%d\nfor winning!\vSent some to MOM!")
-- The half and all texts really are this short on the cart: they replace the -- The half and all texts really are this short on the cart: they replace the
-- money line rather than following it, which is a quirk no Gold player can -- money line rather than following it, which is a quirk no Gold player can
-- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F. -- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F.
+2
View File
@@ -23,4 +23,6 @@ return {
-- (core.asm:426-464): poison/burn/leech seed tick before the slower -- (core.asm:426-464): poison/burn/leech seed tick before the slower
-- mon acts, not in an end-of-round sweep like Gen 3+. -- mon acts, not in an end-of-round sweep like Gen 3+.
residualAfterMove = true, residualAfterMove = true,
-- engine/battle/effects.asm:498,689
badgeBoostReapplyBug = true,
} }
+1 -1
View File
@@ -27,7 +27,7 @@ local BATTLER_FIELDS = {
"chargeReady", "invulnerable", "mustRecharge", "chargeReady", "invulnerable", "mustRecharge",
"thrashTurns", "thrashAnnounced", "focusEnergy", "leechSeeded", "thrashTurns", "thrashAnnounced", "focusEnergy", "leechSeeded",
"lightScreen", "reflect", "mist", "xAccuracy", "lastMove", "flinched", "lightScreen", "reflect", "mist", "xAccuracy", "lastMove", "flinched",
"skipMove", "hazeStatReset", "drainFloor", "drainHold", "trappingTurns", "skipMove", "hazeStatReset", "badgeExtraBoosts", "drainFloor", "drainHold", "trappingTurns",
"trapMove", "trapDamage", "fainted", "trapMove", "trapDamage", "fainted",
"aiLayer2", "aiLayer2",
} }
+19 -1
View File
@@ -6,6 +6,9 @@ local FixedStep = {}
FixedStep.STEP = 1 / 60 FixedStep.STEP = 1 / 60
local MAX_ACCUM = 0.25 -- avoid spiral of death after a stall local MAX_ACCUM = 0.25 -- avoid spiral of death after a stall
local SMOOTH_FRAMES = 4
local SMOOTH_MAX = 1 / 60 * 2.5
local STEP_EPS = 1 / 60 * 0.02
-- Phase the accumulator is re-seeded with once an absorbed hitch frame has -- Phase the accumulator is re-seeded with once an absorbed hitch frame has
-- been paid for. Half a step is the balanced point: a frame has to come in -- been paid for. Half a step is the balanced point: a frame has to come in
@@ -23,6 +26,7 @@ function FixedStep:init(callback)
self.accum = 0 self.accum = 0
self.callback = callback self.callback = callback
self.suppressCatchup = false self.suppressCatchup = false
self.dtHistory, self.dtSum = nil, 0
end end
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15 -- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
@@ -39,12 +43,26 @@ function FixedStep:update(dt)
-- the burst it would otherwise release doesn't play out as a slide. -- the burst it would otherwise release doesn't play out as a slide.
if self.suppressCatchup then if self.suppressCatchup then
self.suppressCatchup = false self.suppressCatchup = false
self.dtHistory, self.dtSum = nil, 0
self.accum = self.STEP * RESEED_PHASE self.accum = self.STEP * RESEED_PHASE
self.callback(self.STEP) self.callback(self.STEP)
return return
end end
if dt > 0 and dt <= SMOOTH_MAX then
local hist = self.dtHistory
if not hist then hist = {}; self.dtHistory = hist; self.dtSum = 0 end
hist[#hist + 1] = dt
self.dtSum = self.dtSum + dt
if #hist > SMOOTH_FRAMES then
self.dtSum = self.dtSum - hist[1]
table.remove(hist, 1)
end
dt = self.dtSum / #hist
else
self.dtHistory, self.dtSum = nil, 0
end
self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM) self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM)
while self.accum >= self.STEP do while self.accum >= self.STEP - STEP_EPS do
self.accum = self.accum - self.STEP self.accum = self.accum - self.STEP
self.callback(self.STEP) self.callback(self.STEP)
end end
+1 -3
View File
@@ -1324,9 +1324,7 @@ function Game:restoreSave(loaded, recovered, opts)
self:adoptSave(loaded) self:adoptSave(loaded)
-- SaveData.load already attached the standalone options.lua table -- SaveData.load already attached the standalone options.lua table
self:applyOptions(loaded.options) self:applyOptions(loaded.options)
-- saves from before OT/ID stamping: backfill with the player's (after -- saves from before OT/ID stamping: backfill with the player's
-- the scrub, so every mon the stamp loop sees is known)
SaveData.repairTradedOtIds(loaded)
local stamp = require("src.battle.BattleState").stampOT local stamp = require("src.battle.BattleState").stampOT
for _, mon in ipairs(loaded.party or {}) do stamp(loaded, mon) end for _, mon in ipairs(loaded.party or {}) do stamp(loaded, mon) end
for _, box in ipairs(loaded.boxes or {}) do for _, box in ipairs(loaded.boxes or {}) do
+4
View File
@@ -350,6 +350,10 @@ function Game2:showTitle()
onContinue = function() onContinue = function()
self:showMainMenu() self:showMainMenu()
end, end,
-- engine/menus/intro_menu.asm:848-889
onTimeout = function()
self:showCopyright()
end,
}) })
end end
+7 -2
View File
@@ -1983,6 +1983,11 @@ SaveData.addCoreMigration(3, function(save)
end end
end) end)
-- #1461 repair, one-shot on pre-format-5 saves
SaveData.addCoreMigration(4, function(save)
SaveData.repairTradedOtIds(save)
end)
-- ------- write -- ------- write
-- Game progress only; options are written separately via saveOptions. -- Game progress only; options are written separately via saveOptions.
@@ -2394,8 +2399,8 @@ function SaveData.applyPostGameHome(save, boot)
end end
-- 0.1.82-0.1.9x loads stamped the player's own id onto traded mons and saved -- 0.1.82-0.1.9x loads stamped the player's own id onto traded mons and saved
-- it, which reads back as home-caught. A caught mon can never carry -- it, which reads back as home-caught. Run only as the pre-format-5
-- traded=true, so clearing that pair is safe on every load. #1461 -- migration: a same-id trade partner makes that pair legitimate. #1461
function SaveData.repairTradedOtIds(save) function SaveData.repairTradedOtIds(save)
local playerId = save and save.player and save.player.id local playerId = save and save.player and save.player.id
if playerId == nil then return 0 end if playerId == nil then return 0 end
-4
View File
@@ -217,10 +217,6 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
local abW = dpadW * 0.46 local abW = dpadW * 0.46
local ssW = dpadW * 0.30 local ssW = dpadW * 0.30
local margin = dpadW * 0.12 local margin = dpadW * 0.12
local ok, GameVersion = pcall(require, "src.core.GameVersion")
if ok and GameVersion.generation and GameVersion.generation() == 2 then
margin = math.max(margin, math.min(ww * 0.10, 72))
end
return { return {
dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW }, dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW },
a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW }, a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW },
+1 -1
View File
@@ -18,7 +18,7 @@ local Version = {
-- exceeds the shell it provides. -- exceeds the shell it provides.
modApi = 2, -- mod API major (manifest `api`) modApi = 2, -- mod API major (manifest `api`)
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL) linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
saveFormat = 4, -- save.meta.format saveFormat = 5, -- save.meta.format
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker) cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
} }
+8 -3
View File
@@ -89,10 +89,12 @@ local function handle(cmd)
return false return false
end end
local idleWait = false
while true do while true do
-- drain every pending command first, so a stop/new-play is seen promptly -- drain every pending command first, so a stop/new-play is seen promptly
local quit = false local quit = false
local cmd = cmdCh:pop() local cmd = idleWait and cmdCh:demand(0.05) or cmdCh:pop()
while cmd do while cmd do
if handle(cmd) then quit = true end if handle(cmd) then quit = true end
cmd = cmdCh:pop() cmd = cmdCh:pop()
@@ -100,6 +102,7 @@ while true do
if quit then break end if quit then break end
if engine and not finished and gen and outCh:getCount() < LOOKAHEAD then if engine and not finished and gen and outCh:getCount() < LOOKAHEAD then
idleWait = false
local activeGen = gen local activeGen = gen
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2) local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
if not ok then if not ok then
@@ -113,8 +116,10 @@ while true do
finished = true finished = true
end end
end end
elseif engine and not finished and gen then
idleWait = false
love.timer.sleep(0.005)
else else
-- nothing to do (idle, or the look-ahead is full): yield the core idleWait = true
love.timer.sleep(0.001)
end end
end end
+5 -7
View File
@@ -164,14 +164,12 @@ local function receiveItemToPc(save, id, data)
save.pcItems = save.pcItems or {} save.pcItems = save.pcItems or {}
local pc = save.pcItems local pc = save.pcItems
local held = pc[id] or 0 local held = pc[id] or 0
if held == 0 then
local cap = (data and data.field and data.field.pcItemCap) or PC_ITEM_CAPACITY local cap = (data and data.field and data.field.pcItemCap) or PC_ITEM_CAPACITY
local stacks = 0 local function stacksFor(n) return math.ceil((n or 0) / MAX_STACK) end
for _ in pairs(pc) do stacks = stacks + 1 end local used = 0
if stacks >= cap then return false end for _, count in pairs(pc) do used = used + stacksFor(count) end
elseif held + 1 > MAX_STACK then -- engine/items/items.asm:156 PutItemInPocket
return false if used + stacksFor(held + 1) - stacksFor(held) > cap then return false end
end
pc[id] = held + 1 pc[id] = held + 1
return true return true
end end
+4 -6
View File
@@ -343,6 +343,7 @@ Save.OPTIONS_KEY = "gold"
local SHARED_KEYS = { local SHARED_KEYS = {
touchControls = true, haptics = true, screenPos = true, touchControls = true, haptics = true, screenPos = true,
videoMode = true,
mods = true, modsByVersion = true, modsGen2 = true, mods = true, modsByVersion = true, modsGen2 = true,
modOptions = true, modProfiles = true, modProfilesSeeded = true, modOptions = true, modProfiles = true, modProfilesSeeded = true,
activeProfile = true, activeProfile = true,
@@ -361,12 +362,9 @@ function Save.loadOptions(fs)
end end
if type(loaded) == "table" then if type(loaded) == "table" then
for key in pairs(SHARED_KEYS) do for key in pairs(SHARED_KEYS) do
if loaded[key] ~= nil then options[key] = loaded[key] end if loaded[key] ~= nil then
end options[key] = loaded[key]
end elseif type(stored) == "table" and stored[key] ~= nil then
if type(stored) == "table" then
for key in pairs(SHARED_KEYS) do
if options[key] == nil and stored[key] ~= nil then
options[key] = stored[key] options[key] = stored[key]
end end
end end
+1 -1
View File
@@ -10,7 +10,7 @@ local CacheContract = {}
CacheContract.FORMAT = "rom-cache-v10:" CacheContract.FORMAT = "rom-cache-v10:"
CacheContract.VERSION_FORMAT = { CacheContract.VERSION_FORMAT = {
crystal = "rom-cache-v10-crystal2:", crystal = "rom-cache-v10-crystal3:",
} }
CacheContract.MARKER_PATH = "rom-cache.complete" CacheContract.MARKER_PATH = "rom-cache.complete"
+12 -24
View File
@@ -23,6 +23,11 @@ local SaveData = require("src.core.SaveData")
local LauncherSettings = {} local LauncherSettings = {}
local function bgLocked(opts)
return opts.battleLayout == "wide" and opts.battleFit == "fill"
and opts.battleHud == "extended"
end
local function wrapIndex(i, n) local function wrapIndex(i, n)
i = i % n i = i % n
if i < 0 then i = i + n end if i < 0 then i = i + n end
@@ -156,8 +161,6 @@ local function coreRows(opts, hooks)
opts.battleLayout = opts.battleLayout == "wide" and "og" or "wide" opts.battleLayout = opts.battleLayout == "wide" and "og" or "wide"
if opts.battleLayout ~= "wide" then if opts.battleLayout ~= "wide" then
opts.battleHud = "standard" opts.battleHud = "standard"
elseif opts.battleFit == "fill" and opts.battleHud == "extended" then
opts.battleBg = "white"
end end
return true return true
end) end)
@@ -167,10 +170,6 @@ local function coreRows(opts, hooks)
end, end,
function() function()
opts.battleFit = opts.battleFit == "fill" and "fixed" or "fill" opts.battleFit = opts.battleFit == "fill" and "fixed" or "fill"
if opts.battleFit == "fill" and opts.battleLayout == "wide"
and opts.battleHud == "extended" then
opts.battleBg = "white"
end
return true return true
end) end)
add(Strings("BATTLE HUD"), add(Strings("BATTLE HUD"),
@@ -185,28 +184,17 @@ local function coreRows(opts, hooks)
return false return false
end end
opts.battleHud = opts.battleHud == "extended" and "standard" or "extended" opts.battleHud = opts.battleHud == "extended" and "standard" or "extended"
if opts.battleHud == "extended" and opts.battleFit == "fill" then
opts.battleBg = "white"
end
return true return true
end) end)
add(Strings("BATTLE BG"), add(Strings("BATTLE BG"),
function() function()
if opts.battleLayout == "wide" and opts.battleFit == "fill" if bgLocked(opts) then return Strings("AUTO (FILL HUD)") end
and opts.battleHud == "extended" then
opts.battleBg = "white"
return Strings("AUTO")
end
if opts.battleBg == "black" then return Strings("BLACK") end if opts.battleBg == "black" then return Strings("BLACK") end
if opts.battleBg == "world" then return Strings("WORLD") end if opts.battleBg == "world" then return Strings("WORLD") end
return Strings("WHITE") return Strings("WHITE")
end, end,
function(dir) function(dir)
if opts.battleLayout == "wide" and opts.battleFit == "fill" if bgLocked(opts) then return false end
and opts.battleHud == "extended" then
opts.battleBg = "white"
return false
end
local order = { "white", "black", "world" } local order = { "white", "black", "world" }
local cur = 1 local cur = 1
for i, mode in ipairs(order) do for i, mode in ipairs(order) do
@@ -563,7 +551,7 @@ end
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync. -- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
local GEN2_KEY = "gold" local GEN2_KEY = "gold"
local function gen2Rows(opts, hooks) local function gen2Rows(opts, hooks, shared)
local rows = {} local rows = {}
local function add(label, value, step) local function add(label, value, step)
rows[#rows + 1] = { label = label, value = value, step = step } rows[#rows + 1] = { label = label, value = value, step = step }
@@ -669,9 +657,9 @@ local function gen2Rows(opts, hooks)
local okVm, VideoMode = pcall(require, "src.core.VideoMode") local okVm, VideoMode = pcall(require, "src.core.VideoMode")
if okVm then if okVm then
add(Strings("VIDEO MODE"), add(Strings("VIDEO MODE"),
function() return VideoMode.modeLabel(opts.videoMode) end, function() return VideoMode.modeLabel(shared.videoMode) end,
function(dir) function(dir)
opts.videoMode = VideoMode.cycle(opts.videoMode, dir) shared.videoMode = VideoMode.cycle(shared.videoMode, dir)
return true return true
end) end)
end end
@@ -690,7 +678,7 @@ local function gen2Rows(opts, hooks)
add(Strings("BATTLE BG"), ladder(opts, "battleBg", add(Strings("BATTLE BG"), ladder(opts, "battleBg",
{ { "white", "WHITE" }, { "black", "BLACK" } }, "white")) { { "white", "WHITE" }, { "black", "BLACK" } }, "white"))
addTouchRows(rows, add, opts, hooks) addTouchRows(rows, add, shared, hooks)
return rows return rows
end end
@@ -718,7 +706,7 @@ function LauncherSettings.open(hooks, version)
opts[GEN2_KEY] = block opts[GEN2_KEY] = block
end end
sections = { sections = {
{ title = Strings("OPTIONS"), rows = gen2Rows(block, hooks) }, { title = Strings("OPTIONS"), rows = gen2Rows(block, hooks, opts) },
} }
else else
sections = { sections = {
+18 -1
View File
@@ -151,6 +151,19 @@ local TEXT_BUFFERS = {
[0xc602] = "wOTTrademonSpeciesName", [0xc602] = "wOTTrademonSpeciesName",
[0xc618] = "wOTTrademonSenderName", [0xc618] = "wOTTrademonSenderName",
} }
-- ../pokecrystal/ram/wram.asm:1925,2333
local TEXT_BUFFERS_CRYSTAL = {
[0xd050] = "wMonOrItemNameBuffer",
[0xd073] = "wStringBuffer1",
[0xd086] = "wStringBuffer2",
[0xd099] = "wStringBuffer3",
[0xd0ac] = "wStringBuffer4",
[0xd0bf] = "wStringBuffer5",
[0xc6d1] = "wPlayerTrademonSpeciesName",
[0xc6e7] = "wPlayerTrademonSenderName",
[0xc703] = "wOTTrademonSpeciesName",
[0xc719] = "wOTTrademonSenderName",
}
-- The text commands that print nothing and carry no argument -- The text commands that print nothing and carry no argument
-- (macros/scripts/text.asm, in TextCommands order): TX_LOW, TX_SCROLL, -- (macros/scripts/text.asm, in TextCommands order): TX_LOW, TX_SCROLL,
-- TX_PAUSE, TX_WAIT_BUTTON, TX_DAY, and the six TX_SOUND_* jingles. -- TX_PAUSE, TX_WAIT_BUTTON, TX_DAY, and the six TX_SOUND_* jingles.
@@ -2295,6 +2308,8 @@ function RomExtractorGen2:extractTitle()
trailBobAmplitude = silver and 3 or 2, trailBobAmplitude = silver and 3 or 2,
trailPhaseStep = silver and 7 or 3, trailPhaseStep = silver and 7 or 3,
trailPhase = silver and 0 or nil, trailPhase = silver and 0 or nil,
-- TitleScreenTimer (engine/menus/intro_menu.asm:951-966).
timeoutFrames = silver and (73 * 60 + 36) or (84 * 60 + 16),
} }
self:write("title", data) self:write("title", data)
return data return data
@@ -2805,6 +2820,8 @@ end
-- is which alongside the text lets a caller fill them in order without -- is which alongside the text lets a caller fill them in order without
-- changing a marker every screen already reads. -- changing a marker every screen already reads.
function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers) function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
local textBuffers = (self.edition == "crystal") and TEXT_BUFFERS_CRYSTAL
or TEXT_BUFFERS
local out = {} local out = {}
local i = 0 local i = 0
local hops = 0 local hops = 0
@@ -2840,7 +2857,7 @@ function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
out[#out + 1] = "{STRBUF}" out[#out + 1] = "{STRBUF}"
if buffers then if buffers then
local target = self.rom:word(bank, address + i + 1) local target = self.rom:word(bank, address + i + 1)
buffers[#buffers + 1] = TEXT_BUFFERS[target] or target buffers[#buffers + 1] = textBuffers[target] or target
end end
i = i + 2 i = i + 2
elseif b == 0x4e or b == 0x4f then elseif b == 0x4e or b == 0x4f then
+5
View File
@@ -310,6 +310,11 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
"Nothing happened!") } "Nothing happened!") }
end end
b.stages[stat] = cur + 1 b.stages[stat] = cur + 1
b.hazeStatReset = nil
if battle.ruleset and battle.ruleset.badgeBoostReapplyBug
and battle.kind ~= "link" then
require("src.battle.Damage").reapplyBadgeBoosts(b, stat)
end
return "consumed", { Strings("%s's\n%s rose!", b.name, Strings(STAT_LABEL[stat])) } return "consumed", { Strings("%s's\n%s rose!", b.name, Strings(STAT_LABEL[stat])) }
end end
-- ItemUseDireHit/ItemUseGuardSpec always set the bit and consume -- ItemUseDireHit/ItemUseGuardSpec always set the bit and consume
+1 -1
View File
@@ -817,7 +817,7 @@ function Renderer:frameRects()
-- GB pixel stops being a whole number of screen pixels, which is the trade -- GB pixel stops being a whole number of screen pixels, which is the trade
-- the setting exists to offer. Clamped on the horizontal too, so a narrow -- the setting exists to offer. Clamped on the horizontal too, so a narrow
-- window scales to fit instead of overflowing off both sides. -- window scales to fit instead of overflowing off both sides.
if self.uiFill then if self.uiFill and not FaithfulRes.scaleCap() then
Up = math.min(ph / uih, pw / uiw) Up = math.min(ph / uih, pw / uiw)
end end
if uiw * Up > pw or uih * Up > ph then if uiw * Up > pw or uih * Up > ph then
+24 -5
View File
@@ -529,6 +529,16 @@ function Commands.set_field(ctx, key, value)
ctx.save[key] = value ctx.save[key] = value
end end
-- scripts/ChampionsRoom.asm:57
function Commands.set_option(ctx, key, value)
local o = ctx.save.options
if not o then
o = {}
ctx.save.options = o
end
o[key] = value
end
function Commands.load_player_starter_name(ctx) function Commands.load_player_starter_name(ctx)
local flags = ctx.save.flags or {} local flags = ctx.save.flags or {}
local species = flags.EVENT_CHOSE_PIKACHU and "PIKACHU" local species = flags.EVENT_CHOSE_PIKACHU and "PIKACHU"
@@ -720,7 +730,8 @@ end
-- Box deposits also print SentToBoxText (give_pokemon.asm:36-37). -- Box deposits also print SentToBoxText (give_pokemon.asm:36-37).
-- skipNickname suppresses AskName for callers that name the gift themselves; -- skipNickname suppresses AskName for callers that name the gift themselves;
-- no vanilla script uses it (pokeyellow scripts/OaksLab.asm, #1013) -- no vanilla script uses it (pokeyellow scripts/OaksLab.asm, #1013)
function Commands.give_pokemon(ctx, species, level, skipNickname) -- gotText prints GotMonText ahead of AskName (give_pokemon.asm:46).
function Commands.give_pokemon(ctx, species, level, skipNickname, gotText)
-- Native mods can transform a gift before the Pokémon object is created. -- Native mods can transform a gift before the Pokémon object is created.
-- This is intentionally an event rather than a special-case starter hook: -- This is intentionally an event rather than a special-case starter hook:
-- mods can use the same seam for story gifts, fossils, or custom scripts. -- mods can use the same seam for story gifts, fossils, or custom scripts.
@@ -754,6 +765,11 @@ function Commands.give_pokemon(ctx, species, level, skipNickname)
ctx.lastCheck = true ctx.lastCheck = true
ctx.addedToParty = addedToParty ctx.addedToParty = addedToParty
ctx.boxNum = boxNum ctx.boxNum = boxNum
-- engine/events/give_pokemon.asm:46
if gotText and ctx.runner then
Commands.text_sound(ctx, "Get_Item1")
Commands.show_text(ctx, "_GotMonText", { RAM = species })
end
-- AskName: both AddPartyMon and SendNewMonToBox; skip mod-set nicks -- AskName: both AddPartyMon and SendNewMonToBox; skip mod-set nicks
-- and callback-style callers with no script runner to yield on. -- and callback-style callers with no script runner to yield on.
if not gift.nickname and not skipNickname and ctx.runner then if not gift.nickname and not skipNickname and ctx.runner then
@@ -1303,11 +1319,14 @@ function FadeOverlay:update()
end end
function FadeOverlay:draw() function FadeOverlay:draw()
if self.color == "white" then local shade = (self.color == "white") and 1 or 0
love.graphics.setColor(1, 1, 1, self.alpha) -- home/fade.asm:26
else local r = self.game and self.game.renderer
love.graphics.setColor(0, 0, 0, self.alpha) if r then
r.screenVeil = { shade, self.alpha }
return
end end
love.graphics.setColor(shade, shade, shade, self.alpha)
love.graphics.rectangle("fill", 0, 0, 160, 144) love.graphics.rectangle("fill", 0, 0, 160, 144)
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
end end
+63 -1
View File
@@ -120,6 +120,67 @@ CallAsm.SITES = {
["04:66d1"] = "TryReceiveItem", ["04:66d1"] = "TryReceiveItem",
} }
-- pokegold-symbols/pokesilver.sym: bank $03 sits two bytes earlier in Silver.
CallAsm.SITES_SILVER = {
["03:4749"] = "GetPartyNickname",
["03:4853"] = "CutDownTreeOrGrass",
["03:4b47"] = "CheckContinueWaterfall",
["03:4d13"] = "SetStrengthFlag",
["03:4d79"] = "TryStrengthOW",
["03:4e1e"] = "DisappearWhirlpool",
["03:4f7d"] = "HasRockSmash",
["03:5094"] = "PutTheRodAway",
["03:506b"] = "Fishing_CheckFacingUp",
["03:51c5"] = "AskCutScript_CheckMap",
}
-- pokecrystal-symbols/pokecrystal.sym: Crystal's own addresses for the same sites.
CallAsm.SITES_CRYSTAL = {
["25:6f76"] = "GiveItemScript_DummyFunction",
["04:65cd"] = "StartMenu",
["04:7327"] = "SelectMenu",
["05:6f5e"] = "OverworldHatchEgg",
["25:6706"] = "EnableWildEncounters",
["24:426f"] = "RingTwice_StartCall",
["24:42eb"] = "HangUp",
["04:53e5"] = "InitCallReceiveDelay",
["24:425c"] = "LoadBillScript",
["24:426a"] = "LoadElmScript",
["3f:5017"] = "MomTriesToBuySomething_ASMFunction",
["04:6599"] = "ItemfinderSound",
["14:46ef"] = "SweetScentEncounter",
["02:431e"] = "TrainerWalkToPlayer",
["14:4753"] = "CheckCanUseSquirtbottle",
["04:764f"] = "SetMemEvent",
["14:4658"] = "PlayPoisonSFX",
["14:467b"] = "CheckWhitedOut",
["04:64fa"] = "OverworldBGMap",
["04:650a"] = "BattleBGMap",
["04:6513"] = "HalveMoney",
["04:6527"] = "GetWhiteoutSpawn",
["03:4706"] = "GetPartyNickname",
["03:4810"] = "CutDownTreeOrGrass",
["23:47e1"] = "BlindingFlash",
["00:3016"] = "HideSprites",
["23:4aed"] = "FlyFromAnim",
["05:54f1"] = "SkipUpdateMapSprites",
["23:4b33"] = "FlyToAnim",
["05:4157"] = "LoadWalkingSpritesGFX",
["03:4b38"] = "CheckContinueWaterfall",
["03:4d12"] = "SetStrengthFlag",
["03:4d78"] = "TryStrengthOW",
["03:4e1d"] = "DisappearWhirlpool",
["23:480a"] = "ShakeHeadbuttTree",
["03:4f7c"] = "HasRockSmash",
["03:5095"] = "PutTheRodAway",
["03:506c"] = "Fishing_CheckFacingUp",
["2e:44b3"] = "LoadFishingGFX",
["03:51ba"] = "AskCutScript_CheckMap",
["2e:41ea"] = "TreeMonEncounter",
["2e:4219"] = "RockMonEncounter",
["04:62f8"] = "TryReceiveItem",
}
-- The three WRAM addresses `memcall` / `memcallasm` / `memjump` take instead of -- The three WRAM addresses `memcall` / `memcallasm` / `memjump` take instead of
-- a routine. Not sites: the pointer AT the address is written at run time -- a routine. Not sites: the pointer AT the address is written at run time
-- (LoadMemScript for the queued script, the phone engine for the other two), -- (LoadMemScript for the queued script, the phone engine for the other two),
@@ -714,7 +775,8 @@ end
-- at import time), and the address pair is the fallback that always works. -- at import time), and the address pair is the fallback that always works.
function CallAsm.nameFor(label, bank, addr) function CallAsm.nameFor(label, bank, addr)
if label and CallAsm.ALL[label] then return label end if label and CallAsm.ALL[label] then return label end
return CallAsm.SITES[CallAsm.key(bank, addr)] local key = CallAsm.key(bank, addr)
return CallAsm.SITES[key] or CallAsm.SITES_SILVER[key] or CallAsm.SITES_CRYSTAL[key]
end end
-- Run a routine by name. Returns the byte the asm leaves in wScriptVar, or -- Run a routine by name. Returns the byte the asm leaves in wScriptVar, or
+35 -14
View File
@@ -8,8 +8,13 @@ local Assets = require("src.render.Assets")
local Font = require("src.render.Font") local Font = require("src.render.Font")
local PaletteFX = require("src.render.PaletteFX") local PaletteFX = require("src.render.PaletteFX")
local Sprites = require("src.pokemon.Sprites") local Sprites = require("src.pokemon.Sprites")
local SpriteRenderer = require("src.render.SpriteRenderer")
local Strings = require("src.core.Strings") local Strings = require("src.core.Strings")
-- engine/events/diploma.asm:65
local OBP0_90 = { { 255, 255, 255 }, { 255, 255, 255 },
{ 170, 170, 170 }, { 85, 85, 85 } }
local Diploma = {} local Diploma = {}
Diploma.__index = Diploma Diploma.__index = Diploma
Diploma.isOpaque = true Diploma.isOpaque = true
@@ -49,20 +54,18 @@ local function drawFrameBox(frame, tx, ty, tw, th)
local img = frame.img local img = frame.img
local q = frame.quads local q = frame.quads
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
-- corners -- engine/link/cable_club.asm:937
love.graphics.draw(img, q[0], tx * 8, ty * 8) love.graphics.draw(img, q[2], tx * 8, ty * 8)
love.graphics.draw(img, q[2], (tx + tw - 1) * 8, ty * 8) love.graphics.draw(img, q[4], (tx + tw - 1) * 8, ty * 8)
love.graphics.draw(img, q[6], tx * 8, (ty + th - 1) * 8) love.graphics.draw(img, q[6], tx * 8, (ty + th - 1) * 8)
love.graphics.draw(img, q[8], (tx + tw - 1) * 8, (ty + th - 1) * 8) love.graphics.draw(img, q[7], (tx + tw - 1) * 8, (ty + th - 1) * 8)
-- horizontal edges
for x = 1, tw - 2 do for x = 1, tw - 2 do
love.graphics.draw(img, q[1], (tx + x) * 8, ty * 8) love.graphics.draw(img, q[3], (tx + x) * 8, ty * 8)
love.graphics.draw(img, q[7], (tx + x) * 8, (ty + th - 1) * 8) love.graphics.draw(img, q[0], (tx + x) * 8, (ty + th - 1) * 8)
end end
-- vertical edges
for y = 1, th - 2 do for y = 1, th - 2 do
love.graphics.draw(img, q[3], tx * 8, (ty + y) * 8) love.graphics.draw(img, q[5], tx * 8, (ty + y) * 8)
love.graphics.draw(img, q[5], (tx + tw - 1) * 8, (ty + y) * 8) love.graphics.draw(img, q[1], (tx + tw - 1) * 8, (ty + y) * 8)
end end
end end
@@ -91,13 +94,31 @@ function Diploma.render(game)
drawFrameBox(frame, 0, 0, 20, 18) drawFrameBox(frame, 0, 0, 20, 18)
-- 2. Draw Player character sprite: farcall DrawPlayerCharacter -- 2. Draw Player character sprite: farcall DrawPlayerCharacter
-- Shifted +33 px right from title screen base (82 + 33 = 115, y = 80) -- engine/movie/title.asm:321
local picPath, picTrueColor = Sprites.playerPath( local title = (game.data and game.data.field and game.data.field.title) or {}
game.data, "front", { kind = "diploma" }) local titlePlayer = title.player
local pic = tryImage(picPath) if type(titlePlayer) == "table" then titlePlayer = titlePlayer.path end
local picPath, picTrueColor = Sprites.playerPic(
titlePlayer or "assets/generated/title/player.png",
{ side = "front", kind = "diploma", data = game.data })
local pic
if picPath then
if picTrueColor then
pic = tryImage(picPath)
else
local ok, faded = pcall(SpriteRenderer.obpImage, picPath, OBP0_90,
"diploma")
pic = (ok and faded) or tryImage(picPath)
end
end
if pic then if pic then
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
local sx, sy, sw, sh = love.graphics.getScissor()
-- engine/events/diploma.asm:44
love.graphics.setScissor(8, 8, 144, 128)
love.graphics.draw(pic, 115, 80) love.graphics.draw(pic, 115, 80)
if sx then love.graphics.setScissor(sx, sy, sw, sh)
else love.graphics.setScissor() end
if picTrueColor then if picTrueColor then
PaletteFX.markTrueColor(115, 80, pic:getDimensions()) PaletteFX.markTrueColor(115, 80, pic:getDimensions())
end end
+17
View File
@@ -95,11 +95,28 @@ function EvolutionState.new(game, mon, newSpecies, onDone, via)
self.t = 0 self.t = 0
self.done = false self.done = false
self.canceled = false self.canceled = false
-- engine/movie/evolution.asm:41-46
Music.stop()
self.crySrc = require("src.core.Sound").playCry(game.data, mon.species)
self.cryT = 0
self.cryWait = self.crySrc ~= nil
if not self.cryWait then
Music.play(game.data, Music.special(game.data, "evolution")) Music.play(game.data, Music.special(game.data, "evolution"))
end
return self return self
end end
function EvolutionState:update(dt) function EvolutionState:update(dt)
if self.cryWait then
self.cryT = self.cryT + 1
local src = self.crySrc
local playing = src and src.isPlaying and src:isPlaying()
if self.cryT >= 3 and (not playing or self.cryT > 180) then
self.cryWait, self.crySrc = false, nil
Music.play(self.game.data, Music.special(self.game.data, "evolution"))
end
return
end
self.t = self.t + 1 self.t = self.t + 1
if self.done then return end if self.done then return end
local game = self.game local game = self.game
+7 -19
View File
@@ -157,6 +157,11 @@ end
-- new SHADER FX 2 row below it -- when both are set, ShaderFX.render() runs -- new SHADER FX 2 row below it -- when both are set, ShaderFX.render() runs
-- main's chain into secondary's, same as stacking two RetroArch presets. -- main's chain into secondary's, same as stacking two RetroArch presets.
local function bgLocked(o)
return o.battleLayout == "wide" and o.battleFit == "fill"
and o.battleHud == "extended"
end
-- the vanilla rows as descriptors; each step body is the old per-index -- the vanilla rows as descriptors; each step body is the old per-index
-- ladder's, so the save.options mutations are unchanged -- ladder's, so the save.options mutations are unchanged
local function buildRows(game) local function buildRows(game)
@@ -197,8 +202,6 @@ local function buildRows(game)
o.battleLayout = o.battleLayout == "wide" and "og" or "wide" o.battleLayout = o.battleLayout == "wide" and "og" or "wide"
if o.battleLayout ~= "wide" then if o.battleLayout ~= "wide" then
o.battleHud = "standard" o.battleHud = "standard"
elseif o.battleFit == "fill" and o.battleHud == "extended" then
o.battleBg = "white"
end end
return true return true
end }, end },
@@ -215,10 +218,6 @@ local function buildRows(game)
step = function(g) step = function(g)
local o = g.save.options local o = g.save.options
o.battleFit = o.battleFit == "fill" and "fixed" or "fill" o.battleFit = o.battleFit == "fill" and "fixed" or "fill"
if o.battleFit == "fill" and o.battleLayout == "wide"
and o.battleHud == "extended" then
o.battleBg = "white"
end
return true return true
end }, end },
{ id = "battleHud", label = Strings("BATTLE HUD"), { id = "battleHud", label = Strings("BATTLE HUD"),
@@ -237,9 +236,6 @@ local function buildRows(game)
return false return false
end end
o.battleHud = o.battleHud == "extended" and "standard" or "extended" o.battleHud = o.battleHud == "extended" and "standard" or "extended"
if o.battleHud == "extended" and o.battleFit == "fill" then
o.battleBg = "white"
end
return true return true
end }, end },
-- What sits behind and around the battle. WHITE is the classic paper -- What sits behind and around the battle. WHITE is the classic paper
@@ -249,11 +245,7 @@ local function buildRows(game)
{ id = "battleBg", label = Strings("BATTLE BG"), { id = "battleBg", label = Strings("BATTLE BG"),
value = function(g) value = function(g)
local o = g.save.options local o = g.save.options
if o.battleLayout == "wide" and o.battleFit == "fill" if bgLocked(o) then return Strings("AUTO (FILL HUD)") end
and o.battleHud == "extended" then
o.battleBg = "white"
return Strings("AUTO")
end
local m = o.battleBg local m = o.battleBg
if m == "black" then return Strings("BLACK") end if m == "black" then return Strings("BLACK") end
if m == "world" then return Strings("WORLD") end if m == "world" then return Strings("WORLD") end
@@ -261,11 +253,7 @@ local function buildRows(game)
end, end,
step = function(g, dir) step = function(g, dir)
local o = g.save.options local o = g.save.options
if o.battleLayout == "wide" and o.battleFit == "fill" if bgLocked(o) then return false end
and o.battleHud == "extended" then
o.battleBg = "white"
return false
end
local order = { "white", "black", "world" } local order = { "white", "black", "world" }
local cur = 1 local cur = 1
for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end
+10 -8
View File
@@ -19,18 +19,23 @@ local function itemName(game, id)
return def and def.name or id return def and def.name or id
end end
local function buildItems(game, store) local function buildItems(game, store, order)
local items = {} local items = {}
local ids = {} local ids = order
if not ids then
ids = {}
for id in pairs(store) do table.insert(ids, id) end for id in pairs(store) do table.insert(ids, id) end
table.sort(ids) table.sort(ids)
end
for _, id in ipairs(ids) do for _, id in ipairs(ids) do
if store[id] then
table.insert(items, { table.insert(items, {
value = id, value = id,
label = itemName(game, id), label = itemName(game, id),
right = "x" .. store[id], right = "x" .. store[id],
}) })
end end
end
return items return items
end end
@@ -105,12 +110,9 @@ local function deposit(game)
local pc = game.save.pcItems local pc = game.save.pcItems
local inv = game.save.inventory local inv = game.save.inventory
local Bag = require("src.inventory.Bag") local Bag = require("src.inventory.Bag")
-- badges live in save.inventory alongside items but are not depositable -- engine/menus/players_pc.asm:99 wListPointer = wNumBagItems, so deposit order == bag order
local depositable = {} local order = Bag.order(game.save, game.data)
for id, count in pairs(inv) do game.stack:push(ListMenu.new(game, "DEPOSIT ITEM", buildItems(game, inv, order), {
if not Bag.isBadge(id) then depositable[id] = count end
end
game.stack:push(ListMenu.new(game, "DEPOSIT ITEM", buildItems(game, depositable), {
kind = "pc_item_deposit", kind = "pc_item_deposit",
messageBox = true, messageBox = true,
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570) noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
+3 -1
View File
@@ -687,8 +687,10 @@ function Studio.deleteEntry(id)
local opts = SaveData.loadOptions() local opts = SaveData.loadOptions()
local tc = type(opts.touchControls) == "table" and opts.touchControls or {} local tc = type(opts.touchControls) == "table" and opts.touchControls or {}
if tc.skin == id then if tc.skin == id then
tc.enabled, tc.skin = false, nil tc.enabled, tc.skin = true, nil
opts.touchControls = tc
SaveData.saveOptions(opts) SaveData.saveOptions(opts)
TouchControls:applyOptions(opts)
end end
Studio.refreshAvailable() Studio.refreshAvailable()
setStatus("Deleted " .. id) setStatus("Deleted " .. id)
+4 -9
View File
@@ -334,10 +334,9 @@ function TownMap:update(dt)
end end
end end
-- OG RED bakes the boot-ROM OBJ palette in, so the marker has to be replayed -- OG RED and ADVANCED bake an OBJ palette in, so the marker replays over the TOWNMAP zone pass (#301)
-- over the screen-wide TOWNMAP zone pass the way every other OBJ is (#301)
function TownMap:markPlayerRedraw(x, y) function TownMap:markPlayerRedraw(x, y)
if not PaletteFX.usesSpriteObp() then return end if not (PaletteFX.usesSpriteObp() or PaletteFX.usesGbcPack()) then return end
PaletteFX.markUiSpriteRedraw(self.playerSheet, self.playerQuad, x, y) PaletteFX.markUiSpriteRedraw(self.playerSheet, self.playerQuad, x, y)
end end
@@ -402,11 +401,7 @@ function TownMap:draw()
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
end end
end end
-- blinking cursor on the selected location. markerXY is the 8x8 cell's -- WriteTownMapSpriteOAM carry quirk: -4 X, -3 Y for cursor and player alike -- engine/items/town_map.asm:454
-- top-left; the cursor asset is a 16x16 hollow frame centered on its own
-- (8,8), so draw it -4,-4 to enclose the cell (engine/menus/town_map.asm
-- draws the box cursor CENTERED on the selected location). Drawing it at
-- the cell top-left put the square in the frame's top-left quadrant (#152).
local showCursor = true local showCursor = true
if GameVersion.generation() == 1 then if GameVersion.generation() == 1 then
showCursor = self.blink < 25 showCursor = self.blink < 25
@@ -416,7 +411,7 @@ function TownMap:draw()
if selected and showCursor then if selected and showCursor then
local x, y = markerXY(selected) local x, y = markerXY(selected)
if self.bg.cursor then if self.bg.cursor then
love.graphics.draw(self.bg.cursor, x - 4, y - 4) love.graphics.draw(self.bg.cursor, x - 4, y - 3)
else else
love.graphics.setColor(0, 0, 0, 1) love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("line", x + 0.5, y + 0.5, 7, 7) love.graphics.rectangle("line", x + 0.5, y + 0.5, 7, 7)
+25 -4
View File
@@ -826,7 +826,10 @@ function BattleState:drawPic(mon, back)
if trainerBack then if trainerBack then
-- PAL_BATTLE_OB_PLAYER: the player's own colours, which are row 0 of -- PAL_BATTLE_OB_PLAYER: the player's own colours, which are row 0 of
-- TrainerPalettes (Chris shares Cal's). -- TrainerPalettes (Chris shares Cal's).
colors = Palettes.trainerColors(self.palettes, "PLAYER") or colors -- engine/gfx/color.asm:683-696
local row = Gen2Save.isFemale(self.save) and "FALKNER" or "PLAYER"
colors = Palettes.trainerColors(self.palettes, row)
or Palettes.trainerColors(self.palettes, "PLAYER") or colors
elseif enemyTrainer then elseif enemyTrainer then
-- The opponent's class row out of the same TrainerPalettes table. -- The opponent's class row out of the same TrainerPalettes table.
colors = Palettes.trainerColors(self.palettes, self.enemyTrainerClass) colors = Palettes.trainerColors(self.palettes, self.enemyTrainerClass)
@@ -1258,7 +1261,15 @@ function BattleState:afterAnimFor(side)
return "ANIM_PLAYER_DAMAGE" return "ANIM_PLAYER_DAMAGE"
end end
function BattleState:animForMove(moveId, side, param) -- engine/battle_anims/anim_commands.asm:1200 PlayHitSound
function BattleState:playHitSound(effectiveness)
if not effectiveness or effectiveness == 0 then return end
if effectiveness > 10 then self:playSfx("Sfx_SuperEffective")
elseif effectiveness < 10 then self:playSfx("Sfx_NotVeryEffective")
else self:playSfx("Sfx_Damage") end
end
function BattleState:animForMove(moveId, side, param, effectiveness)
local key = self.anims and self.anims.moves and self.anims.moves[moveId] local key = self.anims and self.anims.moves and self.anims.moves[moveId]
local started = self:startAnim(key, { local started = self:startAnim(key, {
turn = self:turnFor(side), animId = moveId, isMove = true, param = param, turn = self:turnFor(side), animId = moveId, isMove = true, param = param,
@@ -1267,7 +1278,8 @@ function BattleState:animForMove(moveId, side, param)
-- BattleAnimRunScript (anim_commands.asm:55-72): after the move script -- BattleAnimRunScript (anim_commands.asm:55-72): after the move script
-- restores HUDs it immediately runs wBattleAfterAnim (the hit shake). -- restores HUDs it immediately runs wBattleAfterAnim (the hit shake).
-- Queue it so stepAnim chains without waiting on the next event. -- Queue it so stepAnim chains without waiting on the next event.
self.pendingAfterAnim = { name = self:afterAnimFor(side), side = side } self.pendingAfterAnim = { name = self:afterAnimFor(side), side = side,
effectiveness = effectiveness }
end end
return started return started
end end
@@ -1278,6 +1290,7 @@ function BattleState:startPendingAfterAnim()
if not pending then return false end if not pending then return false end
self.pendingAfterAnim = nil self.pendingAfterAnim = nil
if self:animForId(pending.name, pending.side) then if self:animForId(pending.name, pending.side) then
self:playHitSound(pending.effectiveness)
-- dealDamage's default ANIM_x_DAMAGE is this same shake; skip it there. -- dealDamage's default ANIM_x_DAMAGE is this same shake; skip it there.
self.afterAnimPlayed = true self.afterAnimPlayed = true
return true return true
@@ -1575,6 +1588,11 @@ function BattleState:advanceQueue()
self:showPages(text) self:showPages(text)
return return
end end
-- data/text/battle.asm:184
if event.kind == "money" then
self:showPages(event.text or "")
return
end
-- The shiny sparkle: hBattleTurn 1 and wBattleAnimParam 1 pick -- The shiny sparkle: hBattleTurn 1 and wBattleAnimParam 1 pick
-- BattleAnim_SendOutMon's `.Shiny` arm on the enemy (core.asm:8708-8715). -- BattleAnim_SendOutMon's `.Shiny` arm on the enemy (core.asm:8708-8715).
if event.kind == "shiny-flash" then if event.kind == "shiny-flash" then
@@ -1692,12 +1710,14 @@ function BattleState:advanceQueue()
if event.kind == "move" and not event.missed then if event.kind == "move" and not event.missed then
self.afterAnimPlayed = nil self.afterAnimPlayed = nil
self.pendingAfterAnim = nil self.pendingAfterAnim = nil
if not self:animForMove(event.move, event.side, event.animParam) then if not self:animForMove(event.move, event.side, event.animParam,
event.effectiveness) then
-- BATTLE SCENE off skips the move script but still runs wBattleAfterAnim -- BATTLE SCENE off skips the move script but still runs wBattleAfterAnim
-- (anim_commands.asm:55-72 .disabled fallthrough). -- (anim_commands.asm:55-72 .disabled fallthrough).
local options = self.game and self.game.options local options = self.game and self.game.options
if options and options.battleScene == false then if options and options.battleScene == false then
if self:animForId(self:afterAnimFor(event.side), event.side) then if self:animForId(self:afterAnimFor(event.side), event.side) then
self:playHitSound(event.effectiveness)
self.afterAnimPlayed = true self.afterAnimPlayed = true
end end
end end
@@ -1725,6 +1745,7 @@ function BattleState:advanceQueue()
and (hit == "ANIM_ENEMY_DAMAGE" or hit == "ANIM_PLAYER_DAMAGE") then and (hit == "ANIM_ENEMY_DAMAGE" or hit == "ANIM_PLAYER_DAMAGE") then
self.afterAnimPlayed = nil self.afterAnimPlayed = nil
else else
self:playHitSound(event.effectiveness)
self:animForId(hit, from) self:animForId(hit, from)
end end
end end
+18 -12
View File
@@ -41,6 +41,8 @@ ItemPcMenu.isOpaque = true
local PC_ITEM_CAPACITY = 50 local PC_ITEM_CAPACITY = 50
local MAX_STACK = 99 local MAX_STACK = 99
local function stacksFor(n) return math.ceil((n or 0) / MAX_STACK) end
-- PlayersPCMenuData .PlayersPCMenuPointers strings, verbatim. .WhichPC picks -- PlayersPCMenuData .PlayersPCMenuPointers strings, verbatim. .WhichPC picks
-- which rows a caller sees: PLAYERSPC_NORMAL ends on LOG OFF, PLAYERSPC_HOUSE -- which rows a caller sees: PLAYERSPC_NORMAL ends on LOG OFF, PLAYERSPC_HOUSE
-- carries DECORATION and ends on TURN OFF. -- carries DECORATION and ends on TURN OFF.
@@ -167,18 +169,24 @@ function ItemPcMenu:rebuild()
for id, count in pairs(pc) do for id, count in pairs(pc) do
if (count or 0) > 0 then if (count or 0) > 0 then
local def = self:def(id) local def = self:def(id)
local remaining = count
while remaining > 0 do
local n = math.min(remaining, MAX_STACK)
rows[#rows + 1] = { rows[#rows + 1] = {
id = id, count = count, id = id, count = n,
name = (def and def.name) or id, name = (def and def.name) or id,
index = def and def.index or math.huge, index = def and def.index or math.huge,
} }
remaining = remaining - n
end
end end
end end
-- wPCItems keeps acquisition order; without that recorded, item id order is -- wPCItems keeps acquisition order; without that recorded, item id order is
-- the stable choice, the same sort the PACK uses. -- the stable choice, the same sort the PACK uses.
table.sort(rows, function(a, b) table.sort(rows, function(a, b)
if a.index ~= b.index then return a.index < b.index end if a.index ~= b.index then return a.index < b.index end
return a.id < b.id if a.id ~= b.id then return a.id < b.id end
return a.count > b.count
end) end)
self.rows = rows self.rows = rows
if self.listIndex > #rows + 1 then self.listIndex = #rows + 1 end if self.listIndex > #rows + 1 then self.listIndex = #rows + 1 end
@@ -200,20 +208,18 @@ function ItemPcMenu:ensureVisible()
math.max(0, self:listTotal() - VISIBLE_ROWS))) math.max(0, self:listTotal() - VISIBLE_ROWS)))
end end
-- ReceiveItem over wPCItems: a new id needs one of the fifty stacks, a grown -- ReceiveItem over wPCItems: the add tops up every existing stack of that id
-- one may not pass 99. False is the no-carry the deposit turns into -- and spills the rest into a new one, so it only needs a free stack when the
-- room in place is short. False is the no-carry the deposit turns into
-- _PlayersPCNoRoomDepositText. -- _PlayersPCNoRoomDepositText.
-- engine/items/items.asm:156 PutItemInPocket
function ItemPcMenu:pcAdd(id, qty) function ItemPcMenu:pcAdd(id, qty)
local pc = self.save.pcItems local pc = self.save.pcItems
local held = pc[id] or 0 local held = pc[id] or 0
if held == 0 then local used = 0
local stacks = 0 for _, count in pairs(pc) do used = used + stacksFor(count) end
for _, count in pairs(pc) do local need = stacksFor(held + qty) - stacksFor(held)
if (count or 0) > 0 then stacks = stacks + 1 end if used + need > PC_ITEM_CAPACITY then return false end
end
if stacks >= PC_ITEM_CAPACITY then return false end
end
if held + qty > MAX_STACK then return false end
pc[id] = held + qty pc[id] = held + qty
return true return true
end end
+12 -5
View File
@@ -156,7 +156,9 @@ function TitleState.new(game, opts)
self.entranceHideBelow = entrance and tonumber(entrance.hideBelow) or nil self.entranceHideBelow = entrance and tonumber(entrance.hideBelow) or nil
self.gemY = entrance and (tonumber(title.gemFromY) or -50) or self.gemRestY self.gemY = entrance and (tonumber(title.gemFromY) or -50) or self.gemRestY
self.entranceSfx = title.entranceSfx self.entranceSfx = title.entranceSfx
-- engine/menus/intro_menu.asm:951-966
self.timeoutFrames = tonumber(title.timeoutFrames) self.timeoutFrames = tonumber(title.timeoutFrames)
or ((self.trailMode == "silver") and (73 * 60 + 36) or (84 * 60 + 16))
self.onTimeout = opts.onTimeout self.onTimeout = opts.onTimeout
-- The copyright window line is pal 7 (engine/movie/title.asm:40-43); its -- The copyright window line is pal 7 (engine/movie/title.asm:40-43); its
-- colour 0 backs the whole band. -- colour 0 backs the whole band.
@@ -295,13 +297,18 @@ function TitleState:update(_dt)
return return
end end
-- TitleScreenTimer / TitleScreenMain's run-down back into the attract -- engine/menus/intro_menu.asm:1023-1059
-- loop (engine/menus/intro_menu.asm:1125-1236). if self.timeoutFrames and self.onTimeout then
if self.timeoutFrames and self.onTimeout if self.fadeStart then
and self.frameCounter - (self.timeoutStart or 0) >= self.timeoutFrames then if self.frameCounter - self.fadeStart >= 60 then self.onTimeout() end
self.onTimeout()
return return
end end
if self.frameCounter - (self.timeoutStart or 0) >= self.timeoutFrames then
self.fadeStart = self.frameCounter
Music.fadeOut(8)
return
end
end
local input = self.game.input local input = self.game.input
if input and (input:wasPressed("a") or input:wasPressed("start")) then if input and (input:wasPressed("a") or input:wasPressed("start")) then
+16 -4
View File
@@ -1232,6 +1232,10 @@ function OverworldState:update(dt)
self.player.inputLocked = false self.player.inputLocked = false
end end
end end
if self.spinArrive and not self.player.spinFrames then
self.spinArrive = nil
self.player.inputLocked = false
end
-- EnterMapAnim's .done tail re-enables the companion once the swoop or the -- EnterMapAnim's .done tail re-enables the companion once the swoop or the
-- spin-down has landed (player_animations.asm:40) -- spin-down has landed (player_animations.asm:40)
@@ -1311,7 +1315,7 @@ function OverworldState:update(dt)
local scripted = self.runner:isRunning() or #self.scriptMoves > 0 local scripted = self.runner:isRunning() or #self.scriptMoves > 0
or (self.hopLand or 0) > 0 or (self.hopLand or 0) > 0
or self.engaging or self.emote or self.teleportOut or self.engaging or self.emote or self.teleportOut
or self.flyAnim or self.flyArrive or self.flyAnim or self.flyArrive or self.spinArrive
if not scripted and not self.transitioning then if not scripted and not self.transitioning then
self:checkTrainerSight() self:checkTrainerSight()
-- CheckFightingMapTrainers (home/trainers.asm) zeroes hJoyHeld and -- CheckFightingMapTrainers (home/trainers.asm) zeroes hJoyHeld and
@@ -1321,7 +1325,7 @@ function OverworldState:update(dt)
scripted = self.runner:isRunning() or #self.scriptMoves > 0 scripted = self.runner:isRunning() or #self.scriptMoves > 0
or (self.hopLand or 0) > 0 or (self.hopLand or 0) > 0
or self.engaging or self.emote or self.teleportOut or self.engaging or self.emote or self.teleportOut
or self.flyAnim or self.flyArrive or self.flyAnim or self.flyArrive or self.spinArrive
end end
-- a scriptMove's onDone can push a text box on the frame it retires, and -- a scriptMove's onDone can push a text box on the frame it retires, and
-- DisplayTextID owns the loop from there (home/text_script.asm:3) -- DisplayTextID owns the loop from there (home/text_script.asm:3)
@@ -4802,6 +4806,10 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
self.doorWarp = nil self.doorWarp = nil
local arriveWarp = self.arriveWarp local arriveWarp = self.arriveWarp
self.arriveWarp = nil self.arriveWarp = nil
if self.spinArrive then
self.spinArrive = nil
self.player.inputLocked = false
end
-- PlayMapChangeSound (home/overworld.asm) plays before the tail-called -- PlayMapChangeSound (home/overworld.asm) plays before the tail-called
-- GBFadeOutToBlack, so the SFX starts with the fade (#961) -- GBFadeOutToBlack, so the SFX starts with the fade (#961)
if doorWarp then if doorWarp then
@@ -4849,6 +4857,9 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
self.player.spinFrames = 48 self.player.spinFrames = 48
self.player.spinTotal = 48 self.player.spinTotal = 48
self.player.spinDrop = true self.player.spinDrop = true
-- engine/overworld/player_animations.asm:19
self.spinArrive = true
self.player.inputLocked = true
end end
if doorWarp then if doorWarp then
-- PlayerStepOutFromDoor (engine/overworld/auto_movement.asm): any -- PlayerStepOutFromDoor (engine/overworld/auto_movement.asm): any
@@ -5329,8 +5340,9 @@ function OverworldState:drawWorld()
-- cry with no bubble still pauses the world for its beat) -- cry with no bubble still pauses the world for its beat)
if self.emote.bubble == false then return end if self.emote.bubble == false then return end
local npc = self.emote.npc local npc = self.emote.npc
local ex = npc.px - cam.x + 4 -- engine/overworld/emotion_bubbles.asm:41
local ey = npc.py - cam.y - 14 local ex = npc.px - cam.x
local ey = npc.py - cam.y - 20
local bubble = Game.data.field.emotionBubbles local bubble = Game.data.field.emotionBubbles
local drawn = false local drawn = false
if bubble and bubble.path then if bubble and bubble.path then
+4 -4
View File
@@ -551,6 +551,7 @@ function World.new(game)
lastSfx = nil, lastSfx = nil,
pokePic = nil, pokePic = nil,
pendingSceneScript = false, pendingSceneScript = false,
startedOverworld = false,
-- GBC color state (engine/gfx/color.asm). `daytime` is the resolved -- GBC color state (engine/gfx/color.asm). `daytime` is the resolved
-- MORN/DAY/NITE/DARK the map is currently lit by; clockHour overrides -- MORN/DAY/NITE/DARK the map is currently lit by; clockHour overrides
-- World:hour for drivers and tests, so the palette, the hour windows and -- World:hour for drivers and tests, so the palette, the hour windows and
@@ -9045,10 +9046,9 @@ function World:setMap(mapId, cx, cy, facing, opts)
if not opts.seamless then if not opts.seamless then
self.pendingSceneScript = true self.pendingSceneScript = true
end end
-- StartMap (engine/overworld/events.asm): `farcall InitCallReceiveDelay` on -- engine/overworld/events.asm:98
-- every map entry, connections included -- which is why a player who keeps if not self.startedOverworld and self.game and self.game.save then
-- warping is never rung (src/core/gen2/Phone.lua's receive timer). self.startedOverworld = true
if self.game and self.game.save then
require("src.core.gen2.Phone").onMapLoad(self.game.save, require("src.core.gen2.Phone").onMapLoad(self.game.save,
self:stepContext().phone) self:stepContext().phone)
end end
@@ -23,10 +23,10 @@ return function(game)
fitRow.step(game, 1) fitRow.step(game, 1)
assert(options.battleFit == "fill", "battle size switched to FILL") assert(options.battleFit == "fill", "battle size switched to FILL")
assert(options.battleBg == "white", "FILL + EXTENDED normalized background to WHITE") assert(options.battleBg == "black", "FILL + EXTENDED preserves the stored background")
assert(bgRow.value(game) == "AUTO", "adaptive background is labeled AUTO") assert(bgRow.value(game) == "AUTO (FILL HUD)", "adaptive background is labeled AUTO")
assert(bgRow.step(game, 1) == false, "AUTO background row is locked") assert(bgRow.step(game, 1) == false, "AUTO background row is locked")
assert(options.battleBg == "white", "locked AUTO retains the WHITE value") assert(options.battleBg == "black", "locked AUTO does not overwrite the stored value")
menu.index = bgIndex menu.index = bgIndex
menu.scroll = math.max(0, bgIndex - 5) menu.scroll = math.max(0, bgIndex - 5)
@@ -37,11 +37,11 @@ return function(game)
fitRow.step(game, -1) fitRow.step(game, -1)
assert(options.battleFit == "fixed", "battle size switched back to FIXED") assert(options.battleFit == "fixed", "battle size switched back to FIXED")
assert(bgRow.value(game) == "WHITE", "FIXED exposes the stored WHITE choice") assert(bgRow.value(game) == "BLACK", "FIXED exposes the stored BLACK choice")
assert(bgRow.step(game, 1) == true and options.battleBg == "black",
"FIXED can select BLACK")
assert(bgRow.step(game, 1) == true and options.battleBg == "world", assert(bgRow.step(game, 1) == true and options.battleBg == "world",
"FIXED can select WORLD") "FIXED can select WORLD")
assert(bgRow.step(game, 1) == true and options.battleBg == "white",
"FIXED can select WHITE")
U.wait(2) U.wait(2)
local fixedPath = DIR .. "/fixed_extended_background_choices.png" local fixedPath = DIR .. "/fixed_extended_background_choices.png"
os.remove(fixedPath) os.remove(fixedPath)
@@ -48,7 +48,7 @@ end
-- #1461: saves that passed through 0.1.82-0.1.9x already have the player's -- #1461: saves that passed through 0.1.82-0.1.9x already have the player's
-- own id written onto traded mons, so stamping correctly from now on does -- own id written onto traded mons, so stamping correctly from now on does
-- not help them. The repair runs on every load, not behind a format gate. -- not help them. The repair is a one-shot pre-format-5 migration.
do do
local SaveData = require("src.core.SaveData") local SaveData = require("src.core.SaveData")
local save = newSave() local save = newSave()
+9 -9
View File
@@ -229,16 +229,16 @@ check(runScript("VERMILION_TRADE_HOUSE", "TEXT_VERMILIONTRADEHOUSE_LITTLE_GIRL")
"DUX post-trade script completes") "DUX post-trade script completes")
shownIs({ "_AfterTrade3Text" }, "DUX uses the happy dialogset") shownIs({ "_AfterTrade3Text" }, "DUX uses the happy dialogset")
-- === 9) Celadon Eevee: no confirm prompt, AskName then GotMonText, -- === 9) Celadon Eevee: no confirm prompt, GotMonText then AskName,
-- ball hidden (GivePokemon -> AddPartyMon AskName; script still -- ball hidden (GivePokemon -> SetPokedexOwnedFlag prints GotMonText
-- prints GotMonText after the silent give_pokemon row) === -- before AddPartyMon's AskName) ===
local EEVEE_MAP, EEVEE_BALL = local EEVEE_MAP, EEVEE_BALL =
"CELADON_MANSION_ROOF_HOUSE", "CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" "CELADON_MANSION_ROOF_HOUSE", "CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL"
local EEVEE_TEXT = "TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" local EEVEE_TEXT = "TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL"
Game.save = SaveData.newGame() Game.save = SaveData.newGame()
check(runScript(EEVEE_MAP, EEVEE_TEXT), "Eevee ball script completes") check(runScript(EEVEE_MAP, EEVEE_TEXT), "Eevee ball script completes")
shownIs({ "_DoYouWantToNicknameText", "_GotMonText" }, shownIs({ "_GotMonText", "_DoYouWantToNicknameText" },
"Eevee gives immediately (nickname ask, then GotMonText)") "Eevee gives immediately (GotMonText, then nickname ask)")
eq(#Game.save.party, 1, "Eevee joins the party") eq(#Game.save.party, 1, "Eevee joins the party")
eq(Game.save.party[1].species, "EEVEE", "gift species is EEVEE") eq(Game.save.party[1].species, "EEVEE", "gift species is EEVEE")
eq(Game.save.party[1].level, 25, "Eevee is level 25") eq(Game.save.party[1].level, 25, "Eevee is level 25")
@@ -261,13 +261,13 @@ shownIs({}, "old save: silent")
eq(#Game.save.party, 0, "old save: no Eevee re-gift") eq(#Game.save.party, 0, "old save: no Eevee re-gift")
eq(toggleOf(EEVEE_MAP, EEVEE_BALL), false, "old save: leftover ball hidden") eq(toggleOf(EEVEE_MAP, EEVEE_BALL), false, "old save: leftover ball hidden")
-- === 11) GivePokemon party full, box has room: AskName + SentToBoxText -- === 11) GivePokemon party full, box has room: GotMonText, then AskName
-- (SendNewMonToBox), then script GotMonText; ball hidden === -- + SentToBoxText (SendNewMonToBox); ball hidden ===
Game.save = SaveData.newGame() Game.save = SaveData.newGame()
for i = 1, 6 do Game.save.party[i] = Pokemon.new(Data, "PIDGEY", 5) end for i = 1, 6 do Game.save.party[i] = Pokemon.new(Data, "PIDGEY", 5) end
check(runScript(EEVEE_MAP, EEVEE_TEXT), "full-party Eevee script completes") check(runScript(EEVEE_MAP, EEVEE_TEXT), "full-party Eevee script completes")
shownIs({ "_DoYouWantToNicknameText", "_SentToBoxText", "_GotMonText" }, shownIs({ "_GotMonText", "_DoYouWantToNicknameText", "_SentToBoxText" },
"full-party gift: nickname, sent-to-box, then GotMonText") "full-party gift: GotMonText, nickname, then sent-to-box")
eq(#Game.save.party, 6, "full-party gift leaves party size at 6") eq(#Game.save.party, 6, "full-party gift leaves party size at 6")
local Boxes = require("src.pokemon.Boxes") local Boxes = require("src.pokemon.Boxes")
local boxed = false local boxed = false
+11 -1
View File
@@ -20,7 +20,8 @@ local yellow = GameVersion.isYellow()
-- pokered/scripts/*.asm, PlayCry line in brackets: -- pokered/scripts/*.asm, PlayCry line in brackets:
-- SSAnne1FRooms.asm:66 [:70], SSAnneB1FRooms.asm:82 [:86], -- SSAnne1FRooms.asm:66 [:70], SSAnneB1FRooms.asm:82 [:86],
-- VermilionPidgeyHouse.asm:15 [:19], VermilionCity.asm:224 [:228], -- VermilionPidgeyHouse.asm:15 [:19], VermilionCity.asm:224 [:228],
-- PokemonFanClub.asm:71 [:76] and :84 [:89]. -- PokemonFanClub.asm:71 [:76] and :84 [:89],
-- MrFujisHouse.asm:56 [:60] and :63 [:67], LavenderCuboneHouse.asm:10 [:14].
local PETS = { local PETS = {
{ map = "SS_ANNE_1F_ROOMS", const = "TEXT_SSANNE1FROOMS_WIGGLYTUFF", { map = "SS_ANNE_1F_ROOMS", const = "TEXT_SSANNE1FROOMS_WIGGLYTUFF",
object = "SSANNE1FROOMS_WIGGLYTUFF", species = "WIGGLYTUFF", object = "SSANNE1FROOMS_WIGGLYTUFF", species = "WIGGLYTUFF",
@@ -40,6 +41,15 @@ local PETS = {
{ map = "POKEMON_FAN_CLUB", const = "TEXT_POKEMONFANCLUB_SEEL", { map = "POKEMON_FAN_CLUB", const = "TEXT_POKEMONFANCLUB_SEEL",
object = "POKEMONFANCLUB_SEEL", species = "SEEL", object = "POKEMONFANCLUB_SEEL", species = "SEEL",
label = "_PokemonFanClubSeelText" }, label = "_PokemonFanClubSeelText" },
{ map = "MR_FUJIS_HOUSE", const = "TEXT_MRFUJISHOUSE_PSYDUCK",
object = "MRFUJISHOUSE_PSYDUCK", species = "PSYDUCK",
label = "_MrFujisHousePsyduckText" },
{ map = "MR_FUJIS_HOUSE", const = "TEXT_MRFUJISHOUSE_NIDORINO",
object = "MRFUJISHOUSE_NIDORINO", species = "NIDORINO",
label = "_MrFujisHouseNidorinoText" },
{ map = "LAVENDER_CUBONE_HOUSE", const = "TEXT_LAVENDERCUBONEHOUSE_CUBONE",
object = "LAVENDERCUBONEHOUSE_CUBONE", species = "CUBONE",
label = "_LavenderCuboneHouseCuboneText" },
} }
if yellow then if yellow then